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