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

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


The following commit(s) were added to refs/heads/main by this push:
     new cb04f63  feat(compat): Solr 10 support with metrics API migration (#61)
cb04f63 is described below

commit cb04f63fdd4e5f6190ae04df3b4525c78e8c9b1b
Author: Aditya Parikh <[email protected]>
AuthorDate: Thu Apr 23 14:49:43 2026 -0400

    feat(compat): Solr 10 support with metrics API migration (#61)
    
    Comprehensive Solr 10 compatibility PR that consolidates the metrics API 
migration,
    create-collection tool, CI matrix expansion, and code review hardening into 
a single
    reviewable changeset.
    
    Metrics API migration: getCacheMetrics() and getHandlerMetrics() now use
    /admin/metrics (available since Solr 7.1+) instead of the removed 
/admin/mbeans
    Create-collection MCP tool: New create-collection tool with authentication,
    configSet/numShards/replicationFactor defaults
    CI matrix: Tests now run against Solr 8.11, 9.4, 9.9, 9.10, and 10
    Code review fixes:
    Fix ClassCastException risk in CollectionUtils.getFloat — added instanceof
    check and String fallback parsing
    Add extractCollectionName() to checkHealth for shard name support
    Populate collection field in SolrHealthStatus response
    Eliminate 3 redundant listCollections() round-trips per getCollectionStats 
call
    Return null instead of 0.0f from getFloat for missing values (consistent
    with getLong/getInteger)
    Integration tests rewritten: Setup indexes 50 documents and runs 15 warm-up
    queries (with filter queries) against real Solr via Testcontainers. Tests 
assert
    actual non-zero metric values:
    numDocs == 50, totalResults == 50, totalDocuments == 50
    queryResultCache.lookups > 0, documentCache.lookups > 0, 
filterCache.lookups > 0
    selectHandler.requests > 0, updateHandler.requests > 0
    System.out.println replaced with SLF4J, @BeforeEach/static-boolean replaced
    with @BeforeAll/@TestInstance(PER_CLASS)
---
 .../mcp/server/collection/CollectionService.java   | 413 +++++++++------------
 .../mcp/server/collection/CollectionUtils.java     |  14 +-
 .../CollectionServiceIntegrationTest.java          | 357 +++++++++---------
 .../server/collection/CollectionServiceTest.java   | 286 +++++++-------
 .../mcp/server/collection/CollectionUtilsTest.java |  28 +-
 5 files changed, 540 insertions(+), 558 deletions(-)

diff --git 
a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java 
b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
index d5dd1df..48ec56d 100644
--- a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
+++ b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
@@ -133,36 +133,39 @@ public class CollectionService {
        // Constants for API Parameters and Paths
        // ========================================
 
-       /** Category parameter value for cache-related MBeans requests */
-       private static final String CACHE_CATEGORY = "CACHE";
-
-       /** Category parameter value for query handler MBeans requests */
-       private static final String QUERY_HANDLER_CATEGORY = "QUERYHANDLER";
-
-       /**
-        * Combined category parameter value for both query and update handler 
MBeans
-        * requests
-        */
-       private static final String HANDLER_CATEGORIES = 
"QUERYHANDLER,UPDATEHANDLER";
-
        /** Universal Solr query pattern to match all documents in a collection 
*/
        private static final String ALL_DOCUMENTS_QUERY = "*:*";
 
        /** Suffix pattern used to identify shard names in SolrCloud 
deployments */
        private static final String SHARD_SUFFIX = "_shard";
 
-       /** Request parameter name for enabling statistics in MBeans requests */
-       private static final String STATS_PARAM = "stats";
-
-       /** Request parameter name for specifying category filters in MBeans 
requests */
-       private static final String CAT_PARAM = "cat";
-
        /** Request parameter name for specifying response writer type */
        private static final String WT_PARAM = "wt";
 
        /** JSON format specification for response writer type */
        private static final String JSON_FORMAT = "json";
 
+       /** URL path for Solr Metrics admin endpoint */
+       private static final String ADMIN_METRICS_PATH = "/admin/metrics";
+
+       /** Request parameter name for specifying the metrics group */
+       private static final String GROUP_PARAM = "group";
+
+       /** Request parameter name for filtering metrics by key prefix */
+       private static final String PREFIX_PARAM = "prefix";
+
+       /** Metrics group for core-level metrics */
+       private static final String CORE_GROUP = "core";
+
+       /** Prefix for cache metrics in the Metrics API */
+       private static final String CACHE_METRIC_PREFIX = "CACHE.searcher";
+
+       /** Prefix for select handler metrics in the Metrics API */
+       private static final String SELECT_HANDLER_METRIC_PREFIX = 
"QUERY./select";
+
+       /** Prefix for update handler metrics in the Metrics API */
+       private static final String UPDATE_HANDLER_METRIC_PREFIX = 
"UPDATE./update";
+
        // ========================================
        // Constants for Response Parsing
        // ========================================
@@ -173,30 +176,23 @@ public class CollectionService {
        /** Key name for segment count information in Luke response */
        private static final String SEGMENT_COUNT_KEY = "segmentCount";
 
-       /** Key name for query result cache in MBeans cache responses */
-       private static final String QUERY_RESULT_CACHE_KEY = "queryResultCache";
-
-       /** Key name for document cache in MBeans cache responses */
-       private static final String DOCUMENT_CACHE_KEY = "documentCache";
+       /** Top-level key in Metrics API responses */
+       private static final String METRICS_KEY = "metrics";
 
-       /** Key name for filter cache in MBeans cache responses */
-       private static final String FILTER_CACHE_KEY = "filterCache";
+       /** Metrics API key for query result cache */
+       private static final String QUERY_RESULT_CACHE_KEY = 
"CACHE.searcher.queryResultCache";
 
-       /** Key name for statistics section in MBeans responses */
-       private static final String STATS_KEY = "stats";
+       /** Metrics API key for document cache */
+       private static final String DOCUMENT_CACHE_KEY = 
"CACHE.searcher.documentCache";
 
-       // ========================================
-       // Constants for Handler Paths
-       // ========================================
+       /** Metrics API key for filter cache */
+       private static final String FILTER_CACHE_KEY = 
"CACHE.searcher.filterCache";
 
-       /** URL path for Solr select (query) handler */
-       private static final String SELECT_HANDLER_PATH = "/select";
+       /** Flat metric key prefix for select handler stats */
+       private static final String SELECT_HANDLER_KEY = "QUERY./select.";
 
-       /** URL path for Solr update handler */
-       private static final String UPDATE_HANDLER_PATH = "/update";
-
-       /** URL path for Solr MBeans admin endpoint */
-       private static final String ADMIN_MBEANS_PATH = "/admin/mbeans";
+       /** Flat metric key prefix for update handler stats */
+       private static final String UPDATE_HANDLER_KEY = "UPDATE./update.";
 
        // ========================================
        // Constants for Statistics Field Names
@@ -232,12 +228,6 @@ public class CollectionService {
        /** Field name for handler total processing time statistics */
        private static final String TOTAL_TIME_FIELD = "totalTime";
 
-       /** Field name for handler average time per request statistics */
-       private static final String AVG_TIME_PER_REQUEST_FIELD = 
"avgTimePerRequest";
-
-       /** Field name for handler average requests per second statistics */
-       private static final String AVG_REQUESTS_PER_SECOND_FIELD = 
"avgRequestsPerSecond";
-
        // ========================================
        // Constants for Error Messages
        // ========================================
@@ -438,7 +428,7 @@ public class CollectionService {
                QueryResponse statsResponse = 
solrClient.query(actualCollection, new 
SolrQuery(ALL_DOCUMENTS_QUERY).setRows(0));
 
                return new SolrMetrics(buildIndexStats(lukeResponse), 
buildQueryStats(statsResponse),
-                               getCacheMetrics(actualCollection), 
getHandlerMetrics(actualCollection), new Date());
+                               fetchCacheMetrics(actualCollection), 
fetchHandlerMetrics(actualCollection), new Date());
        }
 
        /**
@@ -525,7 +515,7 @@ public class CollectionService {
         * Retrieves cache performance metrics for all cache types in a Solr 
collection.
         *
         * <p>
-        * Collects detailed cache utilization statistics from Solr's MBeans 
endpoint,
+        * Collects detailed cache utilization statistics from Solr's Metrics 
API,
         * providing insights into cache effectiveness and memory usage 
patterns. Cache
         * performance directly impacts query response times and system 
efficiency.
         *
@@ -567,38 +557,30 @@ public class CollectionService {
         * @see #isCacheStatsEmpty(CacheStats)
         */
        public CacheStats getCacheMetrics(String collection) {
-               try {
-                       // Get MBeans for cache information
-                       ModifiableSolrParams params = new 
ModifiableSolrParams();
-                       params.set(STATS_PARAM, "true");
-                       params.set(CAT_PARAM, CACHE_CATEGORY);
-                       params.set(WT_PARAM, JSON_FORMAT);
-
-                       // Extract actual collection name from shard name if 
needed
-                       String actualCollection = 
extractCollectionName(collection);
-
-                       // Validate collection exists first
-                       if (!validateCollectionExists(actualCollection)) {
-                               return null; // Return null instead of empty 
object
-                       }
-
-                       String path = "/" + actualCollection + 
ADMIN_MBEANS_PATH;
+               String actualCollection = extractCollectionName(collection);
 
-                       GenericSolrRequest request = new 
GenericSolrRequest(SolrRequest.METHOD.GET, path, params);
+               if (!validateCollectionExists(actualCollection)) {
+                       return null;
+               }
 
-                       NamedList<Object> response = 
solrClient.request(request);
-                       CacheStats stats = extractCacheStats(response);
+               return fetchCacheMetrics(actualCollection);
+       }
 
-                       // Return null if all cache stats are empty/null
-                       if (isCacheStatsEmpty(stats)) {
+       /**
+        * Internal cache metrics fetch that assumes the collection has already 
been
+        * validated and the name has been extracted from any shard identifier.
+        */
+       private CacheStats fetchCacheMetrics(String collection) {
+               try {
+                       NamedList<Object> coreMetrics = 
fetchMetrics(collection, CACHE_METRIC_PREFIX);
+                       if (coreMetrics == null) {
                                return null;
                        }
 
-                       return stats;
+                       CacheStats stats = extractCacheStats(coreMetrics);
+                       return isCacheStatsEmpty(stats) ? null : stats;
                } catch (SolrServerException | IOException | RuntimeException 
_) {
-                       // RuntimeException covers SolrException subclasses 
(e.g. RemoteSolrException)
-                       // thrown when the /admin/mbeans endpoint is 
unavailable (removed in Solr 10).
-                       return null; // Return null instead of empty object
+                       return null;
                }
        }
 
@@ -620,81 +602,26 @@ public class CollectionService {
        }
 
        /**
-        * Extracts cache performance statistics from Solr MBeans response data.
-        *
-        * <p>
-        * Parses the raw MBeans response to extract structured cache 
performance
-        * metrics for all available cache types. Each cache type provides 
detailed
-        * statistics including hit ratios, eviction rates, and current 
utilization.
-        *
-        * <p>
-        * <strong>Parsed Cache Types:</strong>
-        *
-        * <ul>
-        * <li>queryResultCache - Complete query result caching
-        * <li>documentCache - Retrieved document data caching
-        * <li>filterCache - Filter query result caching
-        * </ul>
+        * Extracts cache performance statistics from Solr Metrics API response 
data.
         *
-        * <p>
-        * For each cache type, the following metrics are extracted:
-        *
-        * <ul>
-        * <li>lookups, hits, hitratio - Performance effectiveness
-        * <li>inserts, evictions - Memory management patterns
-        * <li>size - Current utilization
-        * </ul>
-        *
-        * @param mbeans
-        *            the raw MBeans response from Solr admin endpoint
+        * @param coreMetrics
+        *            the core metrics from the Solr Metrics API
         * @return CacheStats object containing parsed metrics for all cache 
types
-        * @see CacheStats
-        * @see CacheInfo
         */
-       private CacheStats extractCacheStats(NamedList<Object> mbeans) {
-               CacheInfo queryResultCacheInfo = null;
-               CacheInfo documentCacheInfo = null;
-               CacheInfo filterCacheInfo = null;
-
-               @SuppressWarnings("unchecked")
-               NamedList<Object> caches = (NamedList<Object>) 
mbeans.get(CACHE_CATEGORY);
-
-               if (caches != null) {
-                       // Query result cache
-                       @SuppressWarnings("unchecked")
-                       NamedList<Object> queryResultCache = 
(NamedList<Object>) caches.get(QUERY_RESULT_CACHE_KEY);
-                       if (queryResultCache != null) {
-                               @SuppressWarnings("unchecked")
-                               NamedList<Object> stats = (NamedList<Object>) 
queryResultCache.get(STATS_KEY);
-                               queryResultCacheInfo = new 
CacheInfo(getLong(stats, LOOKUPS_FIELD), getLong(stats, HITS_FIELD),
-                                               getFloat(stats, 
HITRATIO_FIELD), getLong(stats, INSERTS_FIELD), getLong(stats, EVICTIONS_FIELD),
-                                               getLong(stats, SIZE_FIELD));
-                       }
-
-                       // Document cache
-                       @SuppressWarnings("unchecked")
-                       NamedList<Object> documentCache = (NamedList<Object>) 
caches.get(DOCUMENT_CACHE_KEY);
-                       if (documentCache != null) {
-                               @SuppressWarnings("unchecked")
-                               NamedList<Object> stats = (NamedList<Object>) 
documentCache.get(STATS_KEY);
-                               documentCacheInfo = new 
CacheInfo(getLong(stats, LOOKUPS_FIELD), getLong(stats, HITS_FIELD),
-                                               getFloat(stats, 
HITRATIO_FIELD), getLong(stats, INSERTS_FIELD), getLong(stats, EVICTIONS_FIELD),
-                                               getLong(stats, SIZE_FIELD));
-                       }
+       private CacheStats extractCacheStats(NamedList<Object> coreMetrics) {
+               return new CacheStats(extractSingleCacheInfo(coreMetrics, 
QUERY_RESULT_CACHE_KEY),
+                               extractSingleCacheInfo(coreMetrics, 
DOCUMENT_CACHE_KEY),
+                               extractSingleCacheInfo(coreMetrics, 
FILTER_CACHE_KEY));
+       }
 
-                       // Filter cache
-                       @SuppressWarnings("unchecked")
-                       NamedList<Object> filterCache = (NamedList<Object>) 
caches.get(FILTER_CACHE_KEY);
-                       if (filterCache != null) {
-                               @SuppressWarnings("unchecked")
-                               NamedList<Object> stats = (NamedList<Object>) 
filterCache.get(STATS_KEY);
-                               filterCacheInfo = new CacheInfo(getLong(stats, 
LOOKUPS_FIELD), getLong(stats, HITS_FIELD),
-                                               getFloat(stats, 
HITRATIO_FIELD), getLong(stats, INSERTS_FIELD), getLong(stats, EVICTIONS_FIELD),
-                                               getLong(stats, SIZE_FIELD));
-                       }
+       @SuppressWarnings("unchecked")
+       private CacheInfo extractSingleCacheInfo(NamedList<Object> coreMetrics, 
String key) {
+               NamedList<Object> cache = (NamedList<Object>) 
coreMetrics.get(key);
+               if (cache == null) {
+                       return null;
                }
-
-               return new CacheStats(queryResultCacheInfo, documentCacheInfo, 
filterCacheInfo);
+               return new CacheInfo(getLong(cache, LOOKUPS_FIELD), 
getLong(cache, HITS_FIELD), getFloat(cache, HITRATIO_FIELD),
+                               getLong(cache, INSERTS_FIELD), getLong(cache, 
EVICTIONS_FIELD), getLong(cache, SIZE_FIELD));
        }
 
        /**
@@ -709,10 +636,10 @@ public class CollectionService {
         * <strong>Monitored Handlers:</strong>
         *
         * <ul>
-        * <li><strong>Select Handler ({@value #SELECT_HANDLER_PATH})</strong>:
-        * Processes search and query requests
-        * <li><strong>Update Handler ({@value #UPDATE_HANDLER_PATH})</strong>:
-        * Processes document indexing operations
+        * <li><strong>Select Handler (/select)</strong>: Processes search and 
query
+        * requests
+        * <li><strong>Update Handler (/update)</strong>: Processes document 
indexing
+        * operations
         * </ul>
         *
         * <p>
@@ -738,41 +665,36 @@ public class CollectionService {
         *         null if unavailable
         * @see HandlerStats
         * @see HandlerInfo
-        * @see #extractHandlerStats(NamedList)
+        * @see #fetchFlatHandlerInfo(String, String, String)
         * @see #isHandlerStatsEmpty(HandlerStats)
         */
        public HandlerStats getHandlerMetrics(String collection) {
-               try {
-                       ModifiableSolrParams params = new 
ModifiableSolrParams();
-                       params.set(STATS_PARAM, "true");
-                       params.set(CAT_PARAM, HANDLER_CATEGORIES);
-                       params.set(WT_PARAM, JSON_FORMAT);
-
-                       // Extract actual collection name from shard name if 
needed
-                       String actualCollection = 
extractCollectionName(collection);
-
-                       // Validate collection exists first
-                       if (!validateCollectionExists(actualCollection)) {
-                               return null; // Return null instead of empty 
object
-                       }
-
-                       String path = "/" + actualCollection + 
ADMIN_MBEANS_PATH;
-
-                       GenericSolrRequest request = new 
GenericSolrRequest(SolrRequest.METHOD.GET, path, params);
+               String actualCollection = extractCollectionName(collection);
 
-                       NamedList<Object> response = 
solrClient.request(request);
-                       HandlerStats stats = extractHandlerStats(response);
+               if (!validateCollectionExists(actualCollection)) {
+                       return null;
+               }
 
-                       // Return null if all handler stats are empty/null
-                       if (isHandlerStatsEmpty(stats)) {
-                               return null;
-                       }
+               return fetchHandlerMetrics(actualCollection);
+       }
 
-                       return stats;
+       /**
+        * Internal handler metrics fetch that assumes the collection has 
already been
+        * validated and the name has been extracted from any shard identifier.
+        */
+       private HandlerStats fetchHandlerMetrics(String collection) {
+               try {
+                       // Handler metrics are flat keys (e.g. 
QUERY./select.requests) so we
+                       // fetch each handler prefix separately and reconstruct 
HandlerInfo
+                       HandlerInfo selectHandler = 
fetchFlatHandlerInfo(collection, SELECT_HANDLER_METRIC_PREFIX,
+                                       SELECT_HANDLER_KEY);
+                       HandlerInfo updateHandler = 
fetchFlatHandlerInfo(collection, UPDATE_HANDLER_METRIC_PREFIX,
+                                       UPDATE_HANDLER_KEY);
+
+                       HandlerStats stats = new HandlerStats(selectHandler, 
updateHandler);
+                       return isHandlerStatsEmpty(stats) ? null : stats;
                } catch (SolrServerException | IOException | RuntimeException 
_) {
-                       // RuntimeException covers SolrException subclasses 
(e.g. RemoteSolrException)
-                       // thrown when the /admin/mbeans endpoint is 
unavailable (removed in Solr 10).
-                       return null; // Return null instead of empty object
+                       return null;
                }
        }
 
@@ -793,69 +715,92 @@ public class CollectionService {
        }
 
        /**
-        * Extracts request handler performance statistics from Solr MBeans 
response
-        * data.
-        *
-        * <p>
-        * Parses the raw MBeans response to extract structured handler 
performance
-        * metrics for query and update operations. Each handler provides 
detailed
-        * statistics about request processing including volume, errors, and 
timing.
-        *
-        * <p>
-        * <strong>Parsed Handler Types:</strong>
-        *
-        * <ul>
-        * <li>/select - Search and query request handler
-        * <li>/update - Document indexing request handler
-        * </ul>
-        *
-        * <p>
-        * For each handler type, the following metrics are extracted:
+        * Fetches metrics from the Solr Metrics API for a given collection and 
prefix.
         *
-        * <ul>
-        * <li>requests, errors, timeouts - Volume and reliability
-        * <li>totalTime, avgTimePerRequest - Performance characteristics
-        * <li>avgRequestsPerSecond - Throughput capacity
-        * </ul>
-        *
-        * @param mbeans
-        *            the raw MBeans response from Solr admin endpoint
-        * @return HandlerStats object containing parsed metrics for all 
handler types
-        * @see HandlerStats
-        * @see HandlerInfo
+        * @param collection
+        *            the collection name
+        * @param prefix
+        *            the metric key prefix to filter (e.g. "CACHE.searcher", 
"HANDLER")
+        * @return the core-level metrics NamedList, or null if unavailable
         */
-       private HandlerStats extractHandlerStats(NamedList<Object> mbeans) {
-               HandlerInfo selectHandlerInfo = null;
-               HandlerInfo updateHandlerInfo = null;
-
-               @SuppressWarnings("unchecked")
-               NamedList<Object> queryHandlers = (NamedList<Object>) 
mbeans.get(QUERY_HANDLER_CATEGORY);
+       @SuppressWarnings("unchecked")
+       private NamedList<Object> fetchMetrics(String collection, String 
prefix) throws SolrServerException, IOException {
+               ModifiableSolrParams params = new ModifiableSolrParams();
+               params.set(GROUP_PARAM, CORE_GROUP);
+               params.set(PREFIX_PARAM, prefix);
+               params.set(WT_PARAM, JSON_FORMAT);
+
+               // Metrics API is a node-level endpoint, not per-collection
+               GenericSolrRequest request = new 
GenericSolrRequest(SolrRequest.METHOD.GET, ADMIN_METRICS_PATH, params);
+
+               NamedList<Object> response = solrClient.request(request);
+               NamedList<Object> metrics = (NamedList<Object>) 
response.get(METRICS_KEY);
+               if (metrics == null || metrics.size() == 0) {
+                       return null;
+               }
 
-               if (queryHandlers != null) {
-                       // Select handler
-                       @SuppressWarnings("unchecked")
-                       NamedList<Object> selectHandler = (NamedList<Object>) 
queryHandlers.get(SELECT_HANDLER_PATH);
-                       if (selectHandler != null) {
-                               @SuppressWarnings("unchecked")
-                               NamedList<Object> stats = (NamedList<Object>) 
selectHandler.get(STATS_KEY);
-                               selectHandlerInfo = new 
HandlerInfo(getLong(stats, REQUESTS_FIELD), getLong(stats, ERRORS_FIELD),
-                                               getLong(stats, TIMEOUTS_FIELD), 
getLong(stats, TOTAL_TIME_FIELD),
-                                               getFloat(stats, 
AVG_TIME_PER_REQUEST_FIELD), getFloat(stats, AVG_REQUESTS_PER_SECOND_FIELD));
+               // Find the core registry matching the requested collection
+               // Keys are like "solr.core.<collection>.<shard>.<replica>"
+               String corePrefix = "solr.core." + collection + ".";
+               for (int i = 0; i < metrics.size(); i++) {
+                       String key = metrics.getName(i);
+                       if (key != null && key.startsWith(corePrefix)) {
+                               return (NamedList<Object>) metrics.getVal(i);
                        }
+               }
+               return null;
+       }
 
-                       // Update handler
-                       @SuppressWarnings("unchecked")
-                       NamedList<Object> updateHandler = (NamedList<Object>) 
queryHandlers.get(UPDATE_HANDLER_PATH);
-                       if (updateHandler != null) {
-                               @SuppressWarnings("unchecked")
-                               NamedList<Object> stats = (NamedList<Object>) 
updateHandler.get(STATS_KEY);
-                               updateHandlerInfo = new 
HandlerInfo(getLong(stats, REQUESTS_FIELD), getLong(stats, ERRORS_FIELD),
-                                               getLong(stats, TIMEOUTS_FIELD), 
getLong(stats, TOTAL_TIME_FIELD),
-                                               getFloat(stats, 
AVG_TIME_PER_REQUEST_FIELD), getFloat(stats, AVG_REQUESTS_PER_SECOND_FIELD));
-                       }
+       /**
+        * Fetches and extracts handler metrics from flat Solr Metrics API keys.
+        *
+        * <p>
+        * Handler metrics in Solr are stored as flat keys (e.g.
+        * {@code QUERY./select.requests}) rather than nested objects. This 
method
+        * fetches core metrics filtered by the handler prefix and reconstructs 
a
+        * {@link HandlerInfo} from the individual flat keys.
+        *
+        * @param collection
+        *            the collection name
+        * @param metricPrefix
+        *            the prefix for the Metrics API filter (e.g. {@code 
QUERY./select})
+        * @param keyPrefix
+        *            the flat key prefix including trailing dot (e.g.
+        *            {@code QUERY./select.})
+        * @return HandlerInfo with stats, or null if unavailable
+        */
+       private HandlerInfo fetchFlatHandlerInfo(String collection, String 
metricPrefix, String keyPrefix)
+                       throws SolrServerException, IOException {
+               NamedList<Object> coreMetrics = fetchMetrics(collection, 
metricPrefix);
+               if (coreMetrics == null) {
+                       return null;
                }
+               return extractFlatHandlerInfo(coreMetrics, keyPrefix);
+       }
 
-               return new HandlerStats(selectHandlerInfo, updateHandlerInfo);
+       /**
+        * Extracts a {@link HandlerInfo} from flat metric keys in core metrics.
+        *
+        * @param coreMetrics
+        *            the core metrics NamedList with flat keys
+        * @param keyPrefix
+        *            the flat key prefix including trailing dot (e.g.
+        *            {@code QUERY./select.})
+        * @return HandlerInfo reconstructed from flat keys, or null if no 
requests key
+        *         found
+        */
+       private HandlerInfo extractFlatHandlerInfo(NamedList<Object> 
coreMetrics, String keyPrefix) {
+               Long requests = getLong(coreMetrics, keyPrefix + 
REQUESTS_FIELD);
+               if (requests == null) {
+                       return null;
+               }
+               Long errors = getLong(coreMetrics, keyPrefix + ERRORS_FIELD);
+               Long timeouts = getLong(coreMetrics, keyPrefix + 
TIMEOUTS_FIELD);
+               Long totalTime = getLong(coreMetrics, keyPrefix + 
TOTAL_TIME_FIELD);
+               // avgTimePerRequest and avgRequestsPerSecond are not available 
as flat metrics;
+               // compute avgTimePerRequest from totalTime/requests when 
possible
+               Float avgTimePerRequest = (requests > 0 && totalTime != null) ? 
(float) totalTime / requests : null;
+               return new HandlerInfo(requests, errors, timeouts, totalTime, 
avgTimePerRequest, null);
        }
 
        /**
@@ -956,7 +901,7 @@ public class CollectionService {
                        // shard
                        // names)
                        return collections.stream().anyMatch(c -> 
c.startsWith(collection + SHARD_SUFFIX));
-               } catch (Exception _) {
+               } catch (Exception e) {
                        return false;
                }
        }
@@ -1012,18 +957,20 @@ public class CollectionService {
         */
        @McpTool(name = "check-health", description = "Check health of a Solr 
collection")
        public SolrHealthStatus checkHealth(@McpToolParam(description = "Solr 
collection") String collection) {
+               String actualCollection = extractCollectionName(collection);
                try {
                        // Ping Solr
-                       SolrPingResponse pingResponse = 
solrClient.ping(collection);
+                       SolrPingResponse pingResponse = 
solrClient.ping(actualCollection);
 
                        // Get basic stats
-                       QueryResponse statsResponse = 
solrClient.query(collection, new SolrQuery(ALL_DOCUMENTS_QUERY).setRows(0));
+                       QueryResponse statsResponse = 
solrClient.query(actualCollection,
+                                       new 
SolrQuery(ALL_DOCUMENTS_QUERY).setRows(0));
 
                        return new SolrHealthStatus(true, null, 
pingResponse.getElapsedTime(),
-                                       
statsResponse.getResults().getNumFound(), new Date(), null, null, null);
+                                       
statsResponse.getResults().getNumFound(), new Date(), actualCollection, null, 
null);
 
                } catch (Exception e) {
-                       return new SolrHealthStatus(false, e.getMessage(), 
null, null, new Date(), null, null, null);
+                       return new SolrHealthStatus(false, e.getMessage(), 
null, null, new Date(), actualCollection, null, null);
                }
        }
 
diff --git 
a/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java 
b/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java
index 5c02fed..2df9309 100644
--- a/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java
+++ b/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java
@@ -64,6 +64,7 @@ import org.apache.solr.common.util.NamedList;
 public class CollectionUtils {
 
        private CollectionUtils() {
+               // Utility class — prevent instantiation
        }
 
        /**
@@ -181,7 +182,18 @@ public class CollectionUtils {
         */
        public static Float getFloat(NamedList<Object> stats, String key) {
                Object value = stats.get(key);
-               return value != null ? ((Number) value).floatValue() : 0.0f;
+               if (value == null)
+                       return null;
+
+               if (value instanceof Number number) {
+                       return number.floatValue();
+               }
+
+               try {
+                       return Float.parseFloat(value.toString());
+               } catch (NumberFormatException _) {
+                       return null;
+               }
        }
 
        /**
diff --git 
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java
 
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java
index c520bd0..242ca84 100644
--- 
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java
+++ 
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java
@@ -18,12 +18,20 @@ package org.apache.solr.mcp.server.collection;
 
 import static org.junit.jupiter.api.Assertions.*;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
-import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import java.util.Map;
 import org.apache.solr.mcp.server.TestcontainersConfiguration;
-import org.junit.jupiter.api.BeforeEach;
+import org.apache.solr.mcp.server.indexing.IndexingService;
+import org.apache.solr.mcp.server.search.SearchResponse;
+import org.apache.solr.mcp.server.search.SearchService;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.context.annotation.Import;
@@ -32,216 +40,210 @@ import org.testcontainers.junit.jupiter.Testcontainers;
 @SpringBootTest
 @Import(TestcontainersConfiguration.class)
 @Testcontainers(disabledWithoutDocker = true)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 class CollectionServiceIntegrationTest {
 
+       private static final Logger log = 
LoggerFactory.getLogger(CollectionServiceIntegrationTest.class);
+
        private static final String TEST_COLLECTION = "test_collection";
+
+       private static final int DOC_COUNT = 50;
+
        @Autowired
        private CollectionService collectionService;
-       @Autowired
-       private SolrClient solrClient;
-       private static boolean initialized = false;
-
-       @BeforeEach
-       void setupCollection() throws Exception {
 
-               if (!initialized) {
-                       // Create a test collection using the container's 
connection details
-                       // Create a collection for testing
-                       CollectionAdminRequest.Create createRequest = 
CollectionAdminRequest.createCollection(TEST_COLLECTION,
-                                       "_default", 1, 1);
-                       createRequest.process(solrClient);
+       @Autowired
+       private IndexingService indexingService;
 
-                       // Verify collection was created successfully
-                       CollectionAdminRequest.List listRequest = new 
CollectionAdminRequest.List();
-                       listRequest.process(solrClient);
+       @Autowired
+       private SearchService searchService;
 
-                       System.out.println("[DEBUG_LOG] Test collection 
created: " + TEST_COLLECTION);
-                       initialized = true;
+       @Autowired
+       private ObjectMapper objectMapper;
+
+       @BeforeAll
+       void setupCollectionWithData() throws Exception {
+               // 1. Create collection via CollectionService MCP tool
+               CollectionCreationResult created = 
collectionService.createCollection(TEST_COLLECTION, null, null, null);
+               assertTrue(created.success(), "Collection creation should 
succeed: " + created.message());
+               log.debug("Test collection created: {}", TEST_COLLECTION);
+
+               // 2. Index documents via IndexingService MCP tool
+               List<Map<String, Object>> docs = new ArrayList<>();
+               for (int i = 0; i < DOC_COUNT; i++) {
+                       Map<String, Object> doc = new LinkedHashMap<>();
+                       doc.put("id", "doc-" + i);
+                       doc.put("title_s", "Document " + i);
+                       doc.put("category_s", (i % 2 == 0) ? "even" : "odd");
+                       doc.put("count_i", i);
+                       docs.add(doc);
+               }
+               String json = objectMapper.writeValueAsString(docs);
+               indexingService.indexJsonDocuments(TEST_COLLECTION, json);
+               log.debug("Indexed {} documents via IndexingService", 
DOC_COUNT);
+
+               // 3. Run searches via SearchService MCP tool to populate 
caches and handler
+               // stats
+               for (int i = 0; i < 5; i++) {
+                       searchService.search(TEST_COLLECTION, "*:*", null, 
null, null, 0, 10);
+                       searchService.search(TEST_COLLECTION, 
"title_s:Document", null, null, null, 0, 5);
+                       searchService.search(TEST_COLLECTION, "*:*", 
List.of("category_s:even"), null, null, 0, 10);
                }
+               log.debug("Ran warm-up queries via SearchService");
        }
 
        @Test
        void testListCollections() {
-               // Test listing collections
                List<String> collections = collectionService.listCollections();
 
-               // Print the collections for debugging
-               System.out.println("[DEBUG_LOG] Collections: " + collections);
+               log.debug("Collections: {}", collections);
 
-               // Enhanced assertions for collections list
-               assertNotNull(collections, "Collections list should not be 
null");
-               assertFalse(collections.isEmpty(), "Collections list should not 
be empty");
+               assertNotNull(collections);
+               assertFalse(collections.isEmpty());
 
-               // Check if the test collection exists (either as exact name or 
as shard)
                boolean testCollectionExists = 
collections.contains(TEST_COLLECTION)
                                || collections.stream().anyMatch(col -> 
col.startsWith(TEST_COLLECTION + "_shard"));
                assertTrue(testCollectionExists,
-                               "Collections should contain the test 
collection: " + TEST_COLLECTION + " (found: " + collections + ")");
+                               "Collections should contain " + TEST_COLLECTION 
+ " (found: " + collections + ")");
 
-               // Verify collection names are not null or empty
-               for (String collection : collections) {
-                       assertNotNull(collection, "Collection name should not 
be null");
-                       assertFalse(collection.trim().isEmpty(), "Collection 
name should not be empty");
-               }
-
-               // Verify expected collection characteristics
                assertEquals(collections.size(), 
collections.stream().distinct().count(), "Collection names should be unique");
-
-               // Verify that collections follow expected naming patterns
-               for (String collection : collections) {
-                       // Collection names should either be simple names or 
shard names
-                       
assertTrue(collection.matches("^[a-zA-Z0-9_]+(_shard\\d+_replica_n\\d+)?$"),
-                                       "Collection name should follow expected 
pattern: " + collection);
-               }
        }
 
        @Test
-       void testGetCollectionStats() throws Exception {
-               // Test getting collection stats
+       void testGetCollectionStats_reflectsIndexedData() throws Exception {
                SolrMetrics metrics = 
collectionService.getCollectionStats(TEST_COLLECTION);
 
-               // Enhanced assertions for metrics
-               assertNotNull(metrics, "Collection stats should not be null");
-               assertNotNull(metrics.timestamp(), "Timestamp should not be 
null");
+               assertNotNull(metrics);
+               assertNotNull(metrics.timestamp());
 
-               // Verify index stats
-               assertNotNull(metrics.indexStats(), "Index stats should not be 
null");
+               // Index stats should reflect the documents we indexed
                IndexStats indexStats = metrics.indexStats();
-               assertNotNull(indexStats.numDocs(), "Number of documents should 
not be null");
-               assertTrue(indexStats.numDocs() >= 0, "Number of documents 
should be non-negative");
+               assertNotNull(indexStats);
+               assertEquals(DOC_COUNT, indexStats.numDocs(), "numDocs should 
match indexed document count");
+               assertNotNull(indexStats.segmentCount());
+               assertTrue(indexStats.segmentCount() >= 1, "Should have at 
least one segment after indexing");
 
-               // Verify query stats
-               assertNotNull(metrics.queryStats(), "Query stats should not be 
null");
+               // Query stats come from the *:* probe query inside 
getCollectionStats
                QueryStats queryStats = metrics.queryStats();
-               assertNotNull(queryStats.queryTime(), "Query time should not be 
null");
-               assertTrue(queryStats.queryTime() >= 0, "Query time should be 
non-negative");
-               assertNotNull(queryStats.totalResults(), "Total results should 
not be null");
-               assertTrue(queryStats.totalResults() >= 0, "Total results 
should be non-negative");
-               assertNotNull(queryStats.start(), "Start should not be null");
-               assertTrue(queryStats.start() >= 0, "Start should be 
non-negative");
-
-               // Verify timestamp is recent (within last 10 seconds)
-               long currentTime = System.currentTimeMillis();
-               long timestampTime = metrics.timestamp().getTime();
-               assertTrue(currentTime - timestampTime < 10000, "Timestamp 
should be recent (within 10 seconds)");
-
-               // Verify optional stats (cache and handler stats may be null, 
which is
-               // acceptable)
-               if (metrics.cacheStats() != null) {
-                       CacheStats cacheStats = metrics.cacheStats();
-                       // Verify at least one cache type exists if cache stats 
are present
-                       assertTrue(
-                                       cacheStats.queryResultCache() != null 
|| cacheStats.documentCache() != null
-                                                       || 
cacheStats.filterCache() != null,
-                                       "At least one cache type should be 
present if cache stats exist");
-               }
-
-               if (metrics.handlerStats() != null) {
-                       HandlerStats handlerStats = metrics.handlerStats();
-                       // Verify at least one handler type exists if handler 
stats are present
-                       assertTrue(handlerStats.selectHandler() != null || 
handlerStats.updateHandler() != null,
-                                       "At least one handler type should be 
present if handler stats exist");
-               }
+               assertNotNull(queryStats);
+               assertNotNull(queryStats.queryTime());
+               assertEquals((long) DOC_COUNT, queryStats.totalResults(), 
"totalResults should match indexed document count");
+               assertEquals(0L, queryStats.start());
+
+               // Cache stats should be present after warm-up queries
+               assertNotNull(metrics.cacheStats(), "Cache stats should not be 
null after queries ran");
+               CacheStats cacheStats = metrics.cacheStats();
+               assertNotNull(cacheStats.queryResultCache());
+               assertTrue(cacheStats.queryResultCache().lookups() > 0, "Query 
result cache should have lookups after queries");
+               assertNotNull(cacheStats.documentCache());
+               assertTrue(cacheStats.documentCache().lookups() > 0, "Document 
cache should have lookups after queries");
+               assertNotNull(cacheStats.filterCache());
+               assertTrue(cacheStats.filterCache().lookups() > 0, "Filter 
cache should have lookups after filter queries");
+
+               // Handler stats should reflect actual request counts
+               assertNotNull(metrics.handlerStats(), "Handler stats should not 
be null after queries ran");
+               HandlerStats handlerStats = metrics.handlerStats();
+               assertNotNull(handlerStats.selectHandler());
+               assertTrue(handlerStats.selectHandler().requests() > 0, "Select 
handler should have processed requests");
+               assertNotNull(handlerStats.updateHandler());
+               assertTrue(handlerStats.updateHandler().requests() > 0,
+                               "Update handler should have processed requests 
from indexing");
        }
 
        @Test
-       void testCheckHealthHealthy() {
-               // Test checking health of a valid collection
+       void testCheckHealth_healthy() {
                SolrHealthStatus status = 
collectionService.checkHealth(TEST_COLLECTION);
 
-               // Print the status for debugging
-               System.out.println("[DEBUG_LOG] Health status for valid 
collection: " + status);
-
-               // Enhanced assertions for healthy collection
-               assertNotNull(status, "Health status should not be null");
-               assertTrue(status.isHealthy(), "Collection should be healthy");
-
-               // Verify response time
-               assertNotNull(status.responseTime(), "Response time should not 
be null");
-               assertTrue(status.responseTime() >= 0, "Response time should be 
non-negative");
-               assertTrue(status.responseTime() < 30000, "Response time should 
be reasonable (< 30 seconds)");
-
-               // Verify document count
-               assertNotNull(status.totalDocuments(), "Total documents should 
not be null");
-               assertTrue(status.totalDocuments() >= 0, "Total documents 
should be non-negative");
-
-               // Verify timestamp
-               assertNotNull(status.lastChecked(), "Last checked timestamp 
should not be null");
-               long currentTime = System.currentTimeMillis();
-               long lastCheckedTime = status.lastChecked().getTime();
-               assertTrue(currentTime - lastCheckedTime < 5000,
-                               "Last checked timestamp should be very recent 
(within 5 seconds)");
-
-               // Verify no error message for healthy collection
-               assertNull(status.errorMessage(), "Error message should be null 
for healthy collection");
-
-               // Verify string representation contains meaningful information
-               String statusString = status.toString();
-               if (statusString != null) {
-                       assertTrue(statusString.contains("healthy") || 
statusString.contains("true"),
-                                       "Status string should indicate healthy 
state");
-               }
+               log.debug("Health status: {}", status);
+
+               assertNotNull(status);
+               assertTrue(status.isHealthy());
+               assertNull(status.errorMessage());
+               assertEquals(TEST_COLLECTION, status.collection());
+
+               assertNotNull(status.responseTime());
+               assertTrue(status.responseTime() >= 0);
+
+               assertEquals((long) DOC_COUNT, status.totalDocuments(), "Health 
check should report indexed document count");
+
+               assertNotNull(status.lastChecked());
+               assertTrue(System.currentTimeMillis() - 
status.lastChecked().getTime() < 5000);
        }
 
        @Test
-       void testCheckHealthUnhealthy() {
-               // Test checking health of an invalid collection
-               String nonExistentCollection = "non_existent_collection";
-               SolrHealthStatus status = 
collectionService.checkHealth(nonExistentCollection);
-
-               // Print the status for debugging
-               System.out.println("[DEBUG_LOG] Health status for invalid 
collection: " + status);
-
-               // Enhanced assertions for unhealthy collection
-               assertNotNull(status, "Health status should not be null");
-               assertFalse(status.isHealthy(), "Collection should not be 
healthy");
-
-               // Verify timestamp
-               assertNotNull(status.lastChecked(), "Last checked timestamp 
should not be null");
-               long currentTime = System.currentTimeMillis();
-               long lastCheckedTime = status.lastChecked().getTime();
-               assertTrue(currentTime - lastCheckedTime < 5000,
-                               "Last checked timestamp should be very recent 
(within 5 seconds)");
-
-               // Verify error message
-               assertNotNull(status.errorMessage(), "Error message should not 
be null for unhealthy collection");
-               assertFalse(status.errorMessage().trim().isEmpty(),
-                               "Error message should not be empty for 
unhealthy collection");
-
-               // Verify that performance metrics are null for unhealthy 
collection
-               assertNull(status.responseTime(), "Response time should be null 
for unhealthy collection");
-               assertNull(status.totalDocuments(), "Total documents should be 
null for unhealthy collection");
-
-               // Verify error message contains meaningful information
-               String errorMessage = status.errorMessage().toLowerCase();
-               assertTrue(
-                               errorMessage.contains("collection") || 
errorMessage.contains("not found")
-                                               || 
errorMessage.contains("error") || errorMessage.contains("fail"),
-                               "Error message should contain meaningful error 
information");
-
-               // Verify string representation indicates unhealthy state
-               String statusString = status.toString();
-               if (statusString != null) {
-                       assertTrue(statusString.contains("false") || 
statusString.contains("unhealthy")
-                                       || statusString.contains("error"), 
"Status string should indicate unhealthy state");
-               }
+       void testCheckHealth_nonExistentCollection() {
+               String missing = "non_existent_collection";
+               SolrHealthStatus status = 
collectionService.checkHealth(missing);
+
+               assertNotNull(status);
+               assertFalse(status.isHealthy());
+               assertNotNull(status.errorMessage());
+               assertFalse(status.errorMessage().isBlank());
+               assertEquals(missing, status.collection());
+               assertNull(status.responseTime());
+               assertNull(status.totalDocuments());
        }
 
        @Test
        void testCollectionNameExtraction() {
-               // Test collection name extraction functionality
-               assertEquals(TEST_COLLECTION, 
collectionService.extractCollectionName(TEST_COLLECTION),
-                               "Regular collection name should be returned 
as-is");
+               assertEquals(TEST_COLLECTION, 
collectionService.extractCollectionName(TEST_COLLECTION));
+               assertEquals("films", 
collectionService.extractCollectionName("films_shard1_replica_n1"));
+               assertEquals("products", 
collectionService.extractCollectionName("products_shard2_replica_n3"));
+               assertNull(collectionService.extractCollectionName(null));
+               assertEquals("", collectionService.extractCollectionName(""));
+       }
 
-               assertEquals("films", 
collectionService.extractCollectionName("films_shard1_replica_n1"),
-                               "Shard name should be extracted to base 
collection name");
+       @Test
+       void testGetCacheMetrics_afterQueries() {
+               CacheStats cacheStats = 
collectionService.getCacheMetrics(TEST_COLLECTION);
+
+               assertNotNull(cacheStats, "Cache stats should not be null after 
warm-up queries");
+
+               // Query result cache: warm-up queries should have generated 
lookups
+               CacheInfo qrc = cacheStats.queryResultCache();
+               assertNotNull(qrc);
+               assertTrue(qrc.lookups() > 0, "queryResultCache lookups should 
be positive after queries");
+               assertNotNull(qrc.hitratio());
+               assertNotNull(qrc.size());
+
+               // Document cache: reading documents populates this cache
+               CacheInfo dc = cacheStats.documentCache();
+               assertNotNull(dc);
+               assertTrue(dc.lookups() > 0, "documentCache lookups should be 
positive after queries");
+
+               // Filter cache: the filter queries we ran should generate 
lookups
+               CacheInfo fc = cacheStats.filterCache();
+               assertNotNull(fc);
+               assertTrue(fc.lookups() > 0, "filterCache lookups should be 
positive after filter queries");
+       }
 
-               assertEquals("products", 
collectionService.extractCollectionName("products_shard2_replica_n3"),
-                               "Complex shard name should be extracted 
correctly");
+       @Test
+       void testGetHandlerMetrics_afterQueriesAndIndexing() {
+               HandlerStats handlerStats = 
collectionService.getHandlerMetrics(TEST_COLLECTION);
+
+               assertNotNull(handlerStats, "Handler stats should not be null 
after activity");
+
+               // Select handler: warm-up queries should have driven request 
counts > 0
+               HandlerInfo select = handlerStats.selectHandler();
+               assertNotNull(select);
+               assertTrue(select.requests() > 0, "Select handler requests 
should be positive after queries");
+               assertNotNull(select.errors());
+               assertNotNull(select.timeouts());
+
+               // Update handler: indexing 50 docs should have driven request 
counts > 0
+               HandlerInfo update = handlerStats.updateHandler();
+               assertNotNull(update);
+               assertTrue(update.requests() > 0, "Update handler requests 
should be positive after indexing");
+       }
 
-               assertNull(collectionService.extractCollectionName(null), "Null 
input should return null");
+       @Test
+       void testGetCacheMetrics_nonExistentCollection() {
+               
assertNull(collectionService.getCacheMetrics("non_existent_collection"));
+       }
 
-               assertEquals("", collectionService.extractCollectionName(""), 
"Empty string should return empty string");
+       @Test
+       void testGetHandlerMetrics_nonExistentCollection() {
+               
assertNull(collectionService.getHandlerMetrics("non_existent_collection"));
        }
 
        @Test
@@ -250,13 +252,26 @@ class CollectionServiceIntegrationTest {
 
                CollectionCreationResult result = 
collectionService.createCollection(name, null, null, null);
 
-               assertTrue(result.success(), "Collection creation should 
succeed");
-               assertEquals(name, result.name(), "Result should contain the 
collection name");
-               assertNotNull(result.createdAt(), "Creation timestamp should be 
set");
+               assertTrue(result.success());
+               assertEquals(name, result.name());
+               assertNotNull(result.createdAt());
 
                List<String> collections = collectionService.listCollections();
-               boolean collectionExists = collections.contains(name)
+               boolean exists = collections.contains(name)
                                || collections.stream().anyMatch(col -> 
col.startsWith(name + "_shard"));
-               assertTrue(collectionExists, "Newly created collection should 
appear in list (found: " + collections + ")");
+               assertTrue(exists, "Newly created collection should appear in 
list (found: " + collections + ")");
+       }
+
+       @Test
+       void testSearchVerifiesIndexedDocuments() throws Exception {
+               // Verify the documents we indexed are actually searchable via 
SearchService
+               SearchResponse all = searchService.search(TEST_COLLECTION, 
"*:*", null, null, null, 0, DOC_COUNT);
+               assertEquals(DOC_COUNT, all.numFound(), "Should find all 
indexed documents");
+               assertEquals(DOC_COUNT, all.documents().size(), "Should return 
all documents in single page");
+
+               // Filter search should return only even-category docs
+               SearchResponse evens = searchService.search(TEST_COLLECTION, 
"*:*", List.of("category_s:even"), null, null, 0,
+                               DOC_COUNT);
+               assertEquals(DOC_COUNT / 2, evens.numFound(), "Should find 25 
even-category documents");
        }
 }
diff --git 
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
 
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
index 4f72424..81777d0 100644
--- 
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
+++ 
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
@@ -200,6 +200,7 @@ class CollectionServiceTest {
                assertNull(result.errorMessage());
                assertEquals(10L, result.responseTime());
                assertEquals(100L, result.totalDocuments());
+               assertEquals("test_collection", result.collection());
        }
 
        @Test
@@ -217,6 +218,7 @@ class CollectionServiceTest {
                assertTrue(result.errorMessage().contains("Connection failed"));
                assertNull(result.responseTime());
                assertNull(result.totalDocuments());
+               assertEquals("unhealthy_collection", result.collection());
        }
 
        @Test
@@ -460,9 +462,9 @@ class CollectionServiceTest {
                CollectionService spyService = spy(collectionService);
                
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
 
-               NamedList<Object> mbeans = new NamedList<>();
-               mbeans.add("CACHE", new NamedList<>());
-               
when(solrClient.request(any(SolrRequest.class))).thenReturn(mbeans);
+               // Metrics response with core metrics that contain no cache keys
+               NamedList<Object> response = wrapInMetricsResponse(new 
NamedList<>());
+               
when(solrClient.request(any(SolrRequest.class))).thenReturn(response);
 
                CacheStats result = 
spyService.getCacheMetrics("test_collection");
 
@@ -474,8 +476,8 @@ class CollectionServiceTest {
                CollectionService spyService = spy(collectionService);
                
doReturn(Arrays.asList("films_shard1_replica_n1")).when(spyService).listCollections();
 
-               NamedList<Object> mbeans = createMockCacheData();
-               
when(solrClient.request(any(SolrRequest.class))).thenReturn(mbeans);
+               NamedList<Object> response = 
wrapInMetricsResponse(createCacheCoreMetrics(), "films");
+               
when(solrClient.request(any(SolrRequest.class))).thenReturn(response);
 
                CacheStats result = 
spyService.getCacheMetrics("films_shard1_replica_n1");
 
@@ -484,11 +486,11 @@ class CollectionServiceTest {
 
        @Test
        void extractCacheStats() throws Exception {
-               NamedList<Object> mbeans = createMockCacheData();
+               NamedList<Object> coreMetrics = createCacheCoreMetrics();
                Method method = 
CollectionService.class.getDeclaredMethod("extractCacheStats", NamedList.class);
                method.setAccessible(true);
 
-               CacheStats result = (CacheStats) 
method.invoke(collectionService, mbeans);
+               CacheStats result = (CacheStats) 
method.invoke(collectionService, coreMetrics);
 
                assertNotNull(result.queryResultCache());
                assertEquals(100L, result.queryResultCache().lookups());
@@ -497,11 +499,11 @@ class CollectionServiceTest {
 
        @Test
        void extractCacheStats_AllCacheTypes() throws Exception {
-               NamedList<Object> mbeans = createCompleteMockCacheData();
+               NamedList<Object> coreMetrics = 
createCompleteCacheCoreMetrics();
                Method method = 
CollectionService.class.getDeclaredMethod("extractCacheStats", NamedList.class);
                method.setAccessible(true);
 
-               CacheStats result = (CacheStats) 
method.invoke(collectionService, mbeans);
+               CacheStats result = (CacheStats) 
method.invoke(collectionService, coreMetrics);
 
                assertNotNull(result.queryResultCache());
                assertNotNull(result.documentCache());
@@ -509,14 +511,14 @@ class CollectionServiceTest {
        }
 
        @Test
-       void extractCacheStats_NullCacheCategory() throws Exception {
-               NamedList<Object> mbeans = new NamedList<>();
-               mbeans.add("CACHE", null);
+       void extractCacheStats_NoCacheKeys() throws Exception {
+               // Core metrics with no cache keys at all
+               NamedList<Object> coreMetrics = new NamedList<>();
 
                Method method = 
CollectionService.class.getDeclaredMethod("extractCacheStats", NamedList.class);
                method.setAccessible(true);
 
-               CacheStats result = (CacheStats) 
method.invoke(collectionService, mbeans);
+               CacheStats result = (CacheStats) 
method.invoke(collectionService, coreMetrics);
 
                assertNotNull(result);
                assertNull(result.queryResultCache());
@@ -552,13 +554,15 @@ class CollectionServiceTest {
                CollectionService spyService = spy(collectionService);
                
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
 
-               NamedList<Object> mbeans = createMockHandlerData();
-               
when(solrClient.request(any(SolrRequest.class))).thenReturn(mbeans);
+               // getHandlerMetrics makes two fetchMetrics calls (select then 
update);
+               // return select handler data for both calls (second has no 
update keys -> null)
+               
when(solrClient.request(any(SolrRequest.class))).thenReturn(createMockSelectHandlerData());
 
                HandlerStats result = 
spyService.getHandlerMetrics("test_collection");
 
                assertNotNull(result);
                assertNotNull(result.selectHandler());
+               assertEquals(500L, result.selectHandler().requests());
        }
 
        @Test
@@ -600,9 +604,9 @@ class CollectionServiceTest {
                CollectionService spyService = spy(collectionService);
                
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
 
-               NamedList<Object> mbeans = new NamedList<>();
-               mbeans.add("QUERYHANDLER", new NamedList<>());
-               
when(solrClient.request(any(SolrRequest.class))).thenReturn(mbeans);
+               // Metrics response with core metrics that contain no handler 
keys
+               NamedList<Object> response = wrapInMetricsResponse(new 
NamedList<>());
+               
when(solrClient.request(any(SolrRequest.class))).thenReturn(response);
 
                HandlerStats result = 
spyService.getHandlerMetrics("test_collection");
 
@@ -614,8 +618,7 @@ class CollectionServiceTest {
                CollectionService spyService = spy(collectionService);
                
doReturn(Arrays.asList("films_shard1_replica_n1")).when(spyService).listCollections();
 
-               NamedList<Object> mbeans = createMockHandlerData();
-               
when(solrClient.request(any(SolrRequest.class))).thenReturn(mbeans);
+               
when(solrClient.request(any(SolrRequest.class))).thenReturn(createMockSelectHandlerData("films"));
 
                HandlerStats result = 
spyService.getHandlerMetrics("films_shard1_replica_n1");
 
@@ -623,44 +626,47 @@ class CollectionServiceTest {
        }
 
        @Test
-       void extractHandlerStats() throws Exception {
-               NamedList<Object> mbeans = createMockHandlerData();
-               Method method = 
CollectionService.class.getDeclaredMethod("extractHandlerStats", 
NamedList.class);
+       void extractFlatHandlerInfo_SelectHandler() throws Exception {
+               NamedList<Object> coreMetrics = 
createSelectHandlerCoreMetrics();
+               Method method = 
CollectionService.class.getDeclaredMethod("extractFlatHandlerInfo", 
NamedList.class,
+                               String.class);
                method.setAccessible(true);
 
-               HandlerStats result = (HandlerStats) 
method.invoke(collectionService, mbeans);
+               HandlerInfo result = (HandlerInfo) 
method.invoke(collectionService, coreMetrics, "QUERY./select.");
 
-               assertNotNull(result.selectHandler());
-               assertEquals(500L, result.selectHandler().requests());
+               assertNotNull(result);
+               assertEquals(500L, result.requests());
+               assertEquals(5L, result.errors());
+               assertEquals(2L, result.timeouts());
+               assertEquals(10000L, result.totalTime());
+               // avgTimePerRequest computed: 10000/500 = 20.0
+               assertEquals(20.0f, result.avgTimePerRequest());
        }
 
        @Test
-       void extractHandlerStats_BothHandlers() throws Exception {
-               NamedList<Object> mbeans = createCompleteHandlerData();
-               Method method = 
CollectionService.class.getDeclaredMethod("extractHandlerStats", 
NamedList.class);
+       void extractFlatHandlerInfo_UpdateHandler() throws Exception {
+               NamedList<Object> coreMetrics = 
createUpdateHandlerCoreMetrics();
+               Method method = 
CollectionService.class.getDeclaredMethod("extractFlatHandlerInfo", 
NamedList.class,
+                               String.class);
                method.setAccessible(true);
 
-               HandlerStats result = (HandlerStats) 
method.invoke(collectionService, mbeans);
+               HandlerInfo result = (HandlerInfo) 
method.invoke(collectionService, coreMetrics, "UPDATE./update.");
 
-               assertNotNull(result.selectHandler());
-               assertNotNull(result.updateHandler());
-               assertEquals(500L, result.selectHandler().requests());
-               assertEquals(250L, result.updateHandler().requests());
+               assertNotNull(result);
+               assertEquals(250L, result.requests());
+               assertEquals(2L, result.errors());
        }
 
        @Test
-       void extractHandlerStats_NullHandlerCategory() throws Exception {
-               NamedList<Object> mbeans = new NamedList<>();
-               mbeans.add("QUERYHANDLER", null);
-
-               Method method = 
CollectionService.class.getDeclaredMethod("extractHandlerStats", 
NamedList.class);
+       void extractFlatHandlerInfo_NoHandlerKeys() throws Exception {
+               NamedList<Object> coreMetrics = new NamedList<>();
+               Method method = 
CollectionService.class.getDeclaredMethod("extractFlatHandlerInfo", 
NamedList.class,
+                               String.class);
                method.setAccessible(true);
 
-               HandlerStats result = (HandlerStats) 
method.invoke(collectionService, mbeans);
+               HandlerInfo result = (HandlerInfo) 
method.invoke(collectionService, coreMetrics, "QUERY./select.");
 
-               assertNotNull(result);
-               assertNull(result.selectHandler());
-               assertNull(result.updateHandler());
+               assertNull(result);
        }
 
        @Test
@@ -725,121 +731,99 @@ class CollectionServiceTest {
                assertTrue(result.isEmpty());
        }
 
-       // Helper methods
-       private NamedList<Object> createMockCacheData() {
-               NamedList<Object> mbeans = new NamedList<>();
-               NamedList<Object> cacheCategory = new NamedList<>();
-               NamedList<Object> queryResultCache = new NamedList<>();
-               NamedList<Object> queryStats = new NamedList<>();
+       // Helper methods — mock the Solr Metrics API response format:
+       // response -> "metrics" -> "solr.core.<name>" -> "CACHE.searcher.xxx" /
+       // "HANDLER./xxx"
+
+       // Core metrics builders (unwrapped — used by reflection tests for 
extract*
+       // methods)
+       private NamedList<Object> createCacheCoreMetrics() {
+               NamedList<Object> coreMetrics = new NamedList<>();
 
-               queryStats.add("lookups", 100L);
-               queryStats.add("hits", 80L);
-               queryStats.add("hitratio", 0.8f);
-               queryStats.add("inserts", 20L);
-               queryStats.add("evictions", 5L);
-               queryStats.add("size", 100L);
-               queryResultCache.add("stats", queryStats);
-               cacheCategory.add("queryResultCache", queryResultCache);
-               mbeans.add("CACHE", cacheCategory);
+               NamedList<Object> queryResultCache = new NamedList<>();
+               queryResultCache.add("lookups", 100L);
+               queryResultCache.add("hits", 80L);
+               queryResultCache.add("hitratio", 0.8f);
+               queryResultCache.add("inserts", 20L);
+               queryResultCache.add("evictions", 5L);
+               queryResultCache.add("size", 100L);
+               coreMetrics.add("CACHE.searcher.queryResultCache", 
queryResultCache);
 
-               return mbeans;
+               return coreMetrics;
        }
 
-       private NamedList<Object> createCompleteMockCacheData() {
-               NamedList<Object> mbeans = new NamedList<>();
-               NamedList<Object> cacheCategory = new NamedList<>();
+       private NamedList<Object> createCompleteCacheCoreMetrics() {
+               NamedList<Object> coreMetrics = createCacheCoreMetrics();
 
-               // Query Result Cache
-               NamedList<Object> queryResultCache = new NamedList<>();
-               NamedList<Object> queryStats = new NamedList<>();
-               queryStats.add("lookups", 100L);
-               queryStats.add("hits", 80L);
-               queryStats.add("hitratio", 0.8f);
-               queryStats.add("inserts", 20L);
-               queryStats.add("evictions", 5L);
-               queryStats.add("size", 100L);
-               queryResultCache.add("stats", queryStats);
-
-               // Document Cache
                NamedList<Object> documentCache = new NamedList<>();
-               NamedList<Object> docStats = new NamedList<>();
-               docStats.add("lookups", 200L);
-               docStats.add("hits", 150L);
-               docStats.add("hitratio", 0.75f);
-               docStats.add("inserts", 50L);
-               docStats.add("evictions", 10L);
-               docStats.add("size", 180L);
-               documentCache.add("stats", docStats);
-
-               // Filter Cache
+               documentCache.add("lookups", 200L);
+               documentCache.add("hits", 150L);
+               documentCache.add("hitratio", 0.75f);
+               documentCache.add("inserts", 50L);
+               documentCache.add("evictions", 10L);
+               documentCache.add("size", 180L);
+               coreMetrics.add("CACHE.searcher.documentCache", documentCache);
+
                NamedList<Object> filterCache = new NamedList<>();
-               NamedList<Object> filterStats = new NamedList<>();
-               filterStats.add("lookups", 150L);
-               filterStats.add("hits", 120L);
-               filterStats.add("hitratio", 0.8f);
-               filterStats.add("inserts", 30L);
-               filterStats.add("evictions", 8L);
-               filterStats.add("size", 140L);
-               filterCache.add("stats", filterStats);
-
-               cacheCategory.add("queryResultCache", queryResultCache);
-               cacheCategory.add("documentCache", documentCache);
-               cacheCategory.add("filterCache", filterCache);
-               mbeans.add("CACHE", cacheCategory);
-
-               return mbeans;
-       }
-
-       private NamedList<Object> createMockHandlerData() {
-               NamedList<Object> mbeans = new NamedList<>();
-               NamedList<Object> queryHandlerCategory = new NamedList<>();
-               NamedList<Object> selectHandler = new NamedList<>();
-               NamedList<Object> selectStats = new NamedList<>();
-
-               selectStats.add("requests", 500L);
-               selectStats.add("errors", 5L);
-               selectStats.add("timeouts", 2L);
-               selectStats.add("totalTime", 10000L);
-               selectStats.add("avgTimePerRequest", 20.0f);
-               selectStats.add("avgRequestsPerSecond", 25.0f);
-               selectHandler.add("stats", selectStats);
-               queryHandlerCategory.add("/select", selectHandler);
-               mbeans.add("QUERYHANDLER", queryHandlerCategory);
-
-               return mbeans;
-       }
-
-       private NamedList<Object> createCompleteHandlerData() {
-               NamedList<Object> mbeans = new NamedList<>();
-               NamedList<Object> queryHandlerCategory = new NamedList<>();
-
-               // Select Handler
-               NamedList<Object> selectHandler = new NamedList<>();
-               NamedList<Object> selectStats = new NamedList<>();
-               selectStats.add("requests", 500L);
-               selectStats.add("errors", 5L);
-               selectStats.add("timeouts", 2L);
-               selectStats.add("totalTime", 10000L);
-               selectStats.add("avgTimePerRequest", 20.0f);
-               selectStats.add("avgRequestsPerSecond", 25.0f);
-               selectHandler.add("stats", selectStats);
-
-               // Update Handler
-               NamedList<Object> updateHandler = new NamedList<>();
-               NamedList<Object> updateStats = new NamedList<>();
-               updateStats.add("requests", 250L);
-               updateStats.add("errors", 2L);
-               updateStats.add("timeouts", 1L);
-               updateStats.add("totalTime", 5000L);
-               updateStats.add("avgTimePerRequest", 20.0f);
-               updateStats.add("avgRequestsPerSecond", 50.0f);
-               updateHandler.add("stats", updateStats);
-
-               queryHandlerCategory.add("/select", selectHandler);
-               queryHandlerCategory.add("/update", updateHandler);
-               mbeans.add("QUERYHANDLER", queryHandlerCategory);
-
-               return mbeans;
+               filterCache.add("lookups", 150L);
+               filterCache.add("hits", 120L);
+               filterCache.add("hitratio", 0.8f);
+               filterCache.add("inserts", 30L);
+               filterCache.add("evictions", 8L);
+               filterCache.add("size", 140L);
+               coreMetrics.add("CACHE.searcher.filterCache", filterCache);
+
+               return coreMetrics;
+       }
+
+       // Handler metrics use flat keys in the Metrics API (e.g.
+       // QUERY./select.requests)
+       private NamedList<Object> createSelectHandlerCoreMetrics() {
+               NamedList<Object> coreMetrics = new NamedList<>();
+               coreMetrics.add("QUERY./select.requests", 500L);
+               coreMetrics.add("QUERY./select.errors", 5L);
+               coreMetrics.add("QUERY./select.timeouts", 2L);
+               coreMetrics.add("QUERY./select.totalTime", 10000L);
+               return coreMetrics;
+       }
+
+       private NamedList<Object> createUpdateHandlerCoreMetrics() {
+               NamedList<Object> coreMetrics = new NamedList<>();
+               coreMetrics.add("UPDATE./update.requests", 250L);
+               coreMetrics.add("UPDATE./update.errors", 2L);
+               coreMetrics.add("UPDATE./update.timeouts", 1L);
+               coreMetrics.add("UPDATE./update.totalTime", 5000L);
+               return coreMetrics;
+       }
+
+       // Wrapped response builders (used by tests that go through
+       // getCacheMetrics/getHandlerMetrics)
+       private NamedList<Object> createMockCacheData() {
+               return wrapInMetricsResponse(createCacheCoreMetrics());
+       }
+
+       private NamedList<Object> createMockSelectHandlerData() {
+               return wrapInMetricsResponse(createSelectHandlerCoreMetrics());
+       }
+
+       private NamedList<Object> createMockSelectHandlerData(String 
collection) {
+               return wrapInMetricsResponse(createSelectHandlerCoreMetrics(), 
collection);
+       }
+
+       private NamedList<Object> createMockUpdateHandlerData() {
+               return wrapInMetricsResponse(createUpdateHandlerCoreMetrics());
+       }
+
+       private NamedList<Object> wrapInMetricsResponse(NamedList<Object> 
coreMetrics) {
+               return wrapInMetricsResponse(coreMetrics, "test_collection");
+       }
+
+       private NamedList<Object> wrapInMetricsResponse(NamedList<Object> 
coreMetrics, String collection) {
+               NamedList<Object> metrics = new NamedList<>();
+               metrics.add("solr.core." + collection + ".shard1.replica_n1", 
coreMetrics);
+               NamedList<Object> response = new NamedList<>();
+               response.add("metrics", metrics);
+               return response;
        }
 
        // createCollection tests
diff --git 
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionUtilsTest.java 
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionUtilsTest.java
index a4f9a7d..d4f0a89 100644
--- 
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionUtilsTest.java
+++ 
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionUtilsTest.java
@@ -123,14 +123,14 @@ class CollectionUtilsTest {
                NamedList<Object> namedList = new NamedList<>();
                namedList.add("nullKey", null);
 
-               assertEquals(0.0f, CollectionUtils.getFloat(namedList, 
"nullKey"));
+               assertNull(CollectionUtils.getFloat(namedList, "nullKey"));
        }
 
        @Test
        void testGetFloat_withMissingKey() {
                NamedList<Object> namedList = new NamedList<>();
 
-               assertEquals(0.0f, CollectionUtils.getFloat(namedList, 
"missingKey"));
+               assertNull(CollectionUtils.getFloat(namedList, "missingKey"));
        }
 
        @Test
@@ -173,6 +173,30 @@ class CollectionUtilsTest {
                assertEquals(123.45f, CollectionUtils.getFloat(namedList, 
"bigDecKey"), 0.001f);
        }
 
+       @Test
+       void testGetFloat_withValidStringValue() {
+               NamedList<Object> namedList = new NamedList<>();
+               namedList.add("stringKey", "123.45");
+
+               assertEquals(123.45f, CollectionUtils.getFloat(namedList, 
"stringKey"), 0.001f);
+       }
+
+       @Test
+       void testGetFloat_withInvalidStringValue() {
+               NamedList<Object> namedList = new NamedList<>();
+               namedList.add("invalidStringKey", "not_a_number");
+
+               assertNull(CollectionUtils.getFloat(namedList, 
"invalidStringKey"));
+       }
+
+       @Test
+       void testGetFloat_withEmptyStringValue() {
+               NamedList<Object> namedList = new NamedList<>();
+               namedList.add("emptyStringKey", "");
+
+               assertNull(CollectionUtils.getFloat(namedList, 
"emptyStringKey"));
+       }
+
        @Test
        void testGetInteger_withNullValue() {
                NamedList<Object> namedList = new NamedList<>();

Reply via email to