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..c633a17 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";
+ /** Top-level key in Metrics API responses */
+ private static final String METRICS_KEY = "metrics";
- /** Key name for document cache in MBeans cache responses */
- private static final String DOCUMENT_CACHE_KEY = "documentCache";
+ /** Metrics API key for query result cache */
+ private static final String QUERY_RESULT_CACHE_KEY = "CACHE.searcher.queryResultCache";
- /** Key name for filter cache in MBeans cache responses */
- private static final String FILTER_CACHE_KEY = "filterCache";
-
- /** Key name for statistics section in MBeans responses */
- private static final String STATS_KEY = "stats";
-
- // ========================================
- // Constants for Handler Paths
- // ========================================
+ /** Metrics API key for document cache */
+ private static final String DOCUMENT_CACHE_KEY = "CACHE.searcher.documentCache";
- /** URL path for Solr select (query) handler */
- private static final String SELECT_HANDLER_PATH = "/select";
+ /** Metrics API key for filter cache */
+ private static final String FILTER_CACHE_KEY = "CACHE.searcher.filterCache";
- /** URL path for Solr update handler */
- private static final String UPDATE_HANDLER_PATH = "/update";
+ /** Flat metric key prefix for select handler stats */
+ private static final String SELECT_HANDLER_KEY = "QUERY./select.";
- /** 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
// ========================================
@@ -525,7 +515,7 @@ public QueryStats buildQueryStats(QueryResponse response) {
* Retrieves cache performance metrics for all cache types in a Solr collection.
*
*
- * 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.
*
@@ -568,37 +558,21 @@ public QueryStats buildQueryStats(QueryResponse response) {
*/
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
+ return null;
}
- String path = "/" + actualCollection + ADMIN_MBEANS_PATH;
-
- GenericSolrRequest request = new GenericSolrRequest(SolrRequest.METHOD.GET, path, params);
-
- NamedList response = solrClient.request(request);
- CacheStats stats = extractCacheStats(response);
-
- // Return null if all cache stats are empty/null
- if (isCacheStatsEmpty(stats)) {
+ NamedList coreMetrics = fetchMetrics(actualCollection, 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 +594,26 @@ private boolean isCacheStatsEmpty(CacheStats stats) {
}
/**
- * Extracts cache performance statistics from Solr MBeans response data.
- *
- *
- * 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.
- *
- *
- * Parsed Cache Types:
+ * Extracts cache performance statistics from Solr Metrics API response data.
*
- *
- * queryResultCache - Complete query result caching
- * documentCache - Retrieved document data caching
- * filterCache - Filter query result caching
- *
- *
- *
- * For each cache type, the following metrics are extracted:
- *
- *
- * lookups, hits, hitratio - Performance effectiveness
- * inserts, evictions - Memory management patterns
- * size - Current utilization
- *
- *
- * @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 mbeans) {
- CacheInfo queryResultCacheInfo = null;
- CacheInfo documentCacheInfo = null;
- CacheInfo filterCacheInfo = null;
-
- @SuppressWarnings("unchecked")
- NamedList caches = (NamedList) mbeans.get(CACHE_CATEGORY);
-
- if (caches != null) {
- // Query result cache
- @SuppressWarnings("unchecked")
- NamedList queryResultCache = (NamedList) caches.get(QUERY_RESULT_CACHE_KEY);
- if (queryResultCache != null) {
- @SuppressWarnings("unchecked")
- NamedList stats = (NamedList) 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 documentCache = (NamedList) caches.get(DOCUMENT_CACHE_KEY);
- if (documentCache != null) {
- @SuppressWarnings("unchecked")
- NamedList stats = (NamedList) 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 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 filterCache = (NamedList) caches.get(FILTER_CACHE_KEY);
- if (filterCache != null) {
- @SuppressWarnings("unchecked")
- NamedList stats = (NamedList) 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 coreMetrics, String key) {
+ NamedList cache = (NamedList) 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 +628,10 @@ private CacheStats extractCacheStats(NamedList mbeans) {
* Monitored Handlers:
*
*
- * Select Handler ({@value #SELECT_HANDLER_PATH}) :
- * Processes search and query requests
- * Update Handler ({@value #UPDATE_HANDLER_PATH}) :
- * Processes document indexing operations
+ * Select Handler (/select) : Processes search and query
+ * requests
+ * Update Handler (/update) : Processes document indexing
+ * operations
*
*
*
@@ -738,41 +657,28 @@ private CacheStats extractCacheStats(NamedList mbeans) {
* 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);
-
- NamedList response = solrClient.request(request);
- HandlerStats stats = extractHandlerStats(response);
-
- // Return null if all handler stats are empty/null
- if (isHandlerStatsEmpty(stats)) {
return null;
}
- return stats;
+ // Handler metrics are flat keys (e.g. QUERY./select.requests) so we
+ // fetch each handler prefix separately and reconstruct HandlerInfo
+ HandlerInfo selectHandler = fetchFlatHandlerInfo(actualCollection, SELECT_HANDLER_METRIC_PREFIX,
+ SELECT_HANDLER_KEY);
+ HandlerInfo updateHandler = fetchFlatHandlerInfo(actualCollection, 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 +699,92 @@ private boolean isHandlerStatsEmpty(HandlerStats stats) {
}
/**
- * Extracts request handler performance statistics from Solr MBeans response
- * data.
- *
- *
- * 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.
- *
- *
- * Parsed Handler Types:
- *
- *
- * /select - Search and query request handler
- * /update - Document indexing request handler
- *
- *
- *
- * For each handler type, the following metrics are extracted:
- *
- *
- * requests, errors, timeouts - Volume and reliability
- * totalTime, avgTimePerRequest - Performance characteristics
- * avgRequestsPerSecond - Throughput capacity
- *
+ * Fetches metrics from the Solr Metrics API for a given collection and prefix.
*
- * @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 mbeans) {
- HandlerInfo selectHandlerInfo = null;
- HandlerInfo updateHandlerInfo = null;
-
- @SuppressWarnings("unchecked")
- NamedList queryHandlers = (NamedList) mbeans.get(QUERY_HANDLER_CATEGORY);
+ @SuppressWarnings("unchecked")
+ private NamedList 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 response = solrClient.request(request);
+ NamedList metrics = (NamedList) response.get(METRICS_KEY);
+ if (metrics == null || metrics.size() == 0) {
+ return null;
+ }
- if (queryHandlers != null) {
- // Select handler
- @SuppressWarnings("unchecked")
- NamedList selectHandler = (NamedList) queryHandlers.get(SELECT_HANDLER_PATH);
- if (selectHandler != null) {
- @SuppressWarnings("unchecked")
- NamedList stats = (NamedList) 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..."
+ 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) metrics.getVal(i);
}
+ }
+ return null;
+ }
- // Update handler
- @SuppressWarnings("unchecked")
- NamedList updateHandler = (NamedList) queryHandlers.get(UPDATE_HANDLER_PATH);
- if (updateHandler != null) {
- @SuppressWarnings("unchecked")
- NamedList stats = (NamedList) 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.
+ *
+ *
+ * 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 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 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 +885,7 @@ private boolean validateCollectionExists(String collection) {
// shard
// names)
return collections.stream().anyMatch(c -> c.startsWith(collection + SHARD_SUFFIX));
- } catch (Exception _) {
+ } catch (Exception e) {
return false;
}
}
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..06fd671 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 @@
public class CollectionUtils {
private CollectionUtils() {
+ // Utility class — prevent instantiation
}
/**
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..6141d0e 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
@@ -177,10 +177,8 @@ void testCheckHealthHealthy() {
// 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");
- }
+ assertTrue(statusString.contains("healthy") || statusString.contains("true"),
+ "Status string should indicate healthy state");
}
@Test
@@ -244,6 +242,48 @@ void testCollectionNameExtraction() {
assertEquals("", collectionService.extractCollectionName(""), "Empty string should return empty string");
}
+ @Test
+ void testGetCacheMetrics() {
+ CacheStats cacheStats = collectionService.getCacheMetrics(TEST_COLLECTION);
+
+ assertNotNull(cacheStats, "Cache stats should not be null for an existing collection");
+
+ // Solr registers all three caches at collection creation
+ assertNotNull(cacheStats.queryResultCache(), "Query result cache should not be null");
+ assertTrue(cacheStats.queryResultCache().lookups() >= 0, "Query result cache lookups should be non-negative");
+
+ assertNotNull(cacheStats.documentCache(), "Document cache should not be null");
+ assertTrue(cacheStats.documentCache().lookups() >= 0, "Document cache lookups should be non-negative");
+
+ assertNotNull(cacheStats.filterCache(), "Filter cache should not be null");
+ assertTrue(cacheStats.filterCache().lookups() >= 0, "Filter cache lookups should be non-negative");
+ }
+
+ @Test
+ void testGetHandlerMetrics() {
+ HandlerStats handlerStats = collectionService.getHandlerMetrics(TEST_COLLECTION);
+
+ assertNotNull(handlerStats, "Handler stats should not be null for an existing collection");
+
+ assertNotNull(handlerStats.selectHandler(), "Select handler should not be null");
+ assertTrue(handlerStats.selectHandler().requests() >= 0, "Select handler requests should be non-negative");
+
+ assertNotNull(handlerStats.updateHandler(), "Update handler should not be null");
+ assertTrue(handlerStats.updateHandler().requests() >= 0, "Update handler requests should be non-negative");
+ }
+
+ @Test
+ void testGetCacheMetrics_NonExistentCollection() {
+ CacheStats result = collectionService.getCacheMetrics("non_existent_collection");
+ assertNull(result, "Cache metrics for non-existent collection should be null");
+ }
+
+ @Test
+ void testGetHandlerMetrics_NonExistentCollection() {
+ HandlerStats result = collectionService.getHandlerMetrics("non_existent_collection");
+ assertNull(result, "Handler metrics for non-existent collection should be null");
+ }
+
@Test
void createCollection_createsAndListable() throws Exception {
String name = "mcp_test_create_" + System.currentTimeMillis();
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..b28b2ce 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
@@ -460,9 +460,9 @@ void getCacheMetrics_EmptyStats() throws Exception {
CollectionService spyService = spy(collectionService);
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
- NamedList 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 response = wrapInMetricsResponse(new NamedList<>());
+ when(solrClient.request(any(SolrRequest.class))).thenReturn(response);
CacheStats result = spyService.getCacheMetrics("test_collection");
@@ -474,8 +474,8 @@ void getCacheMetrics_WithShardName() throws Exception {
CollectionService spyService = spy(collectionService);
doReturn(Arrays.asList("films_shard1_replica_n1")).when(spyService).listCollections();
- NamedList mbeans = createMockCacheData();
- when(solrClient.request(any(SolrRequest.class))).thenReturn(mbeans);
+ NamedList response = wrapInMetricsResponse(createCacheCoreMetrics(), "films");
+ when(solrClient.request(any(SolrRequest.class))).thenReturn(response);
CacheStats result = spyService.getCacheMetrics("films_shard1_replica_n1");
@@ -484,11 +484,11 @@ void getCacheMetrics_WithShardName() throws Exception {
@Test
void extractCacheStats() throws Exception {
- NamedList mbeans = createMockCacheData();
+ NamedList 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 +497,11 @@ void extractCacheStats() throws Exception {
@Test
void extractCacheStats_AllCacheTypes() throws Exception {
- NamedList mbeans = createCompleteMockCacheData();
+ NamedList 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 +509,14 @@ void extractCacheStats_AllCacheTypes() throws Exception {
}
@Test
- void extractCacheStats_NullCacheCategory() throws Exception {
- NamedList mbeans = new NamedList<>();
- mbeans.add("CACHE", null);
+ void extractCacheStats_NoCacheKeys() throws Exception {
+ // Core metrics with no cache keys at all
+ NamedList 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 +552,15 @@ void getHandlerMetrics_Success() throws Exception {
CollectionService spyService = spy(collectionService);
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
- NamedList 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 +602,9 @@ void getHandlerMetrics_EmptyStats() throws Exception {
CollectionService spyService = spy(collectionService);
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
- NamedList 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 response = wrapInMetricsResponse(new NamedList<>());
+ when(solrClient.request(any(SolrRequest.class))).thenReturn(response);
HandlerStats result = spyService.getHandlerMetrics("test_collection");
@@ -614,8 +616,7 @@ void getHandlerMetrics_WithShardName() throws Exception {
CollectionService spyService = spy(collectionService);
doReturn(Arrays.asList("films_shard1_replica_n1")).when(spyService).listCollections();
- NamedList 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 +624,47 @@ void getHandlerMetrics_WithShardName() throws Exception {
}
@Test
- void extractHandlerStats() throws Exception {
- NamedList mbeans = createMockHandlerData();
- Method method = CollectionService.class.getDeclaredMethod("extractHandlerStats", NamedList.class);
+ void extractFlatHandlerInfo_SelectHandler() throws Exception {
+ NamedList 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 mbeans = createCompleteHandlerData();
- Method method = CollectionService.class.getDeclaredMethod("extractHandlerStats", NamedList.class);
+ void extractFlatHandlerInfo_UpdateHandler() throws Exception {
+ NamedList 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 mbeans = new NamedList<>();
- mbeans.add("QUERYHANDLER", null);
-
- Method method = CollectionService.class.getDeclaredMethod("extractHandlerStats", NamedList.class);
+ void extractFlatHandlerInfo_NoHandlerKeys() throws Exception {
+ NamedList 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 +729,99 @@ void listCollections_IOError() throws Exception {
assertTrue(result.isEmpty());
}
- // Helper methods
- private NamedList createMockCacheData() {
- NamedList mbeans = new NamedList<>();
- NamedList cacheCategory = new NamedList<>();
- NamedList queryResultCache = new NamedList<>();
- NamedList queryStats = new NamedList<>();
+ // Helper methods — mock the Solr Metrics API response format:
+ // response -> "metrics" -> "solr.core." -> "CACHE.searcher.xxx" /
+ // "HANDLER./xxx"
+
+ // Core metrics builders (unwrapped — used by reflection tests for extract*
+ // methods)
+ private NamedList createCacheCoreMetrics() {
+ NamedList 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 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 createCompleteMockCacheData() {
- NamedList mbeans = new NamedList<>();
- NamedList cacheCategory = new NamedList<>();
+ private NamedList createCompleteCacheCoreMetrics() {
+ NamedList coreMetrics = createCacheCoreMetrics();
- // Query Result Cache
- NamedList queryResultCache = new NamedList<>();
- NamedList 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 documentCache = new NamedList<>();
- NamedList 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 filterCache = new NamedList<>();
- NamedList 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 createMockHandlerData() {
- NamedList mbeans = new NamedList<>();
- NamedList queryHandlerCategory = new NamedList<>();
- NamedList selectHandler = new NamedList<>();
- NamedList 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 createCompleteHandlerData() {
- NamedList mbeans = new NamedList<>();
- NamedList queryHandlerCategory = new NamedList<>();
-
- // Select Handler
- NamedList selectHandler = new NamedList<>();
- NamedList 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 updateHandler = new NamedList<>();
- NamedList 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 createSelectHandlerCoreMetrics() {
+ NamedList 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 createUpdateHandlerCoreMetrics() {
+ NamedList 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 createMockCacheData() {
+ return wrapInMetricsResponse(createCacheCoreMetrics());
+ }
+
+ private NamedList createMockSelectHandlerData() {
+ return wrapInMetricsResponse(createSelectHandlerCoreMetrics());
+ }
+
+ private NamedList createMockSelectHandlerData(String collection) {
+ return wrapInMetricsResponse(createSelectHandlerCoreMetrics(), collection);
+ }
+
+ private NamedList createMockUpdateHandlerData() {
+ return wrapInMetricsResponse(createUpdateHandlerCoreMetrics());
+ }
+
+ private NamedList wrapInMetricsResponse(NamedList coreMetrics) {
+ return wrapInMetricsResponse(coreMetrics, "test_collection");
+ }
+
+ private NamedList wrapInMetricsResponse(NamedList coreMetrics, String collection) {
+ NamedList metrics = new NamedList<>();
+ metrics.add("solr.core." + collection + ".shard1.replica_n1", coreMetrics);
+ NamedList response = new NamedList<>();
+ response.add("metrics", metrics);
+ return response;
}
// createCollection tests