Преглед на файлове

[HealthAPI] Add size parameter that controls the number of affected resources returned (#92399)

This adds a `size` parameter that controls the maximum number of
returned affected resources. The parameter defaults to `1000`, must be
positive, and less than `10_000`
Andrei Dan преди 2 години
родител
ревизия
3723af3ccd
променени са 26 файла, в които са добавени 391 реда и са изтрити 102 реда
  1. 7 0
      docs/changelog/92399.yaml
  2. 6 0
      docs/reference/health/health.asciidoc
  3. 1 1
      qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/HealthRestCancellationIT.java
  4. 12 7
      rest-api-spec/src/main/resources/rest-api-spec/api/_internal.health.json
  5. 2 1
      server/src/internalClusterTest/java/org/elasticsearch/discovery/StableMasterDisruptionIT.java
  6. 7 5
      server/src/internalClusterTest/java/org/elasticsearch/health/GetHealthActionIT.java
  7. 2 2
      server/src/internalClusterTest/java/org/elasticsearch/health/HealthServiceIT.java
  8. 1 1
      server/src/internalClusterTest/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceIT.java
  9. 1 1
      server/src/internalClusterTest/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceIT.java
  10. 1 1
      server/src/main/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorService.java
  11. 5 3
      server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java
  12. 27 11
      server/src/main/java/org/elasticsearch/health/GetHealthAction.java
  13. 5 1
      server/src/main/java/org/elasticsearch/health/HealthIndicatorService.java
  14. 12 7
      server/src/main/java/org/elasticsearch/health/HealthService.java
  15. 4 1
      server/src/main/java/org/elasticsearch/health/RestGetHealthAction.java
  16. 12 11
      server/src/main/java/org/elasticsearch/health/node/DiskHealthIndicatorService.java
  17. 7 2
      server/src/main/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorService.java
  18. 106 36
      server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java
  19. 27 0
      server/src/test/java/org/elasticsearch/health/GetHealthRequestTests.java
  20. 1 1
      server/src/test/java/org/elasticsearch/health/HealthIndicatorServiceTests.java
  21. 18 4
      server/src/test/java/org/elasticsearch/health/HealthServiceTests.java
  22. 81 0
      server/src/test/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceTests.java
  23. 37 0
      server/src/test/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceTests.java
  24. 3 3
      x-pack/plugin/core/src/internalClusterTest/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierShardAvailabilityHealthIndicatorIT.java
  25. 1 1
      x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java
  26. 5 2
      x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java

+ 7 - 0
docs/changelog/92399.yaml

@@ -0,0 +1,7 @@
+pr: 92399
+summary: "[HealthAPI] Add size parameter that controls the number of affected resources\
+  \ returned"
+area: Health
+type: feature
+issues:
+ - 91930

+ 6 - 0
docs/reference/health/health.asciidoc

@@ -92,6 +92,12 @@ for health status set `verbose` to `false` to disable the more expensive analysi
     These details include additional troubleshooting metrics and sometimes a root cause analysis of a health status.
     Defaults to `true`.
 
+`size`::
+    (Optional, integer) The maximum number of affected resources to return.
+    As a diagnosis can return multiple types of affected resources this parameter will limit the number of resources returned for each type to the configured value (e.g. a diagnosis could return
+    `1000` affected indices and `1000` affected nodes).
+    Defaults to `1000`.
+
 [role="child_attributes"]
 [[health-api-response-body]]
 ==== {api-response-body-title}

+ 1 - 1
qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/HealthRestCancellationIT.java

@@ -111,7 +111,7 @@ public class HealthRestCancellationIT extends HttpSmokeTestCase {
         }
 
         @Override
-        public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) {
+        public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) {
             try {
                 operationBlock.acquire();
             } catch (InterruptedException e) {

+ 12 - 7
rest-api-spec/src/main/resources/rest-api-spec/api/_internal.health.json

@@ -32,14 +32,19 @@
       ]
     },
     "params":{
-      "timeout":{
-        "type":"time",
-        "description":"Explicit operation timeout"
+      "timeout": {
+        "type": "time",
+        "description": "Explicit operation timeout"
       },
-      "verbose":{
-        "type":"boolean",
-        "description":"Opt in for more information about the health of the system",
-        "default":true
+      "verbose": {
+        "type": "boolean",
+        "description": "Opt in for more information about the health of the system",
+        "default": true
+      },
+      "size": {
+        "type": "int",
+        "description": "Limit the number of affected resources the health API returns",
+        "default": 1000
       }
     }
   }

+ 2 - 1
server/src/internalClusterTest/java/org/elasticsearch/discovery/StableMasterDisruptionIT.java

@@ -135,7 +135,8 @@ public class StableMasterDisruptionIT extends ESIntegTestCase {
 
     private void assertMasterStability(Client client, HealthStatus expectedStatus, Matcher<String> expectedMatcher) throws Exception {
         assertBusy(() -> {
-            GetHealthAction.Response healthResponse = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(true)).get();
+            GetHealthAction.Response healthResponse = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(true, 1000))
+                .get();
             String debugInformation = xContentToString(healthResponse);
             assertThat(debugInformation, healthResponse.findIndicator("master_is_stable").status(), equalTo(expectedStatus));
             assertThat(debugInformation, healthResponse.findIndicator("master_is_stable").symptom(), expectedMatcher);

+ 7 - 5
server/src/internalClusterTest/java/org/elasticsearch/health/GetHealthActionIT.java

@@ -144,7 +144,7 @@ public class GetHealthActionIT extends ESIntegTestCase {
         }
 
         @Override
-        public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) {
+        public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) {
             var status = clusterService.getClusterSettings().get(statusSetting);
             return createIndicator(
                 status,
@@ -203,8 +203,10 @@ public class GetHealthActionIT extends ESIntegTestCase {
             {
                 ExecutionException exception = expectThrows(
                     ExecutionException.class,
-                    () -> client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(NONEXISTENT_INDICATOR_NAME, randomBoolean()))
-                        .get()
+                    () -> client.execute(
+                        GetHealthAction.INSTANCE,
+                        new GetHealthAction.Request(NONEXISTENT_INDICATOR_NAME, randomBoolean(), 1000)
+                    ).get()
                 );
                 assertThat(exception.getCause(), instanceOf(ResourceNotFoundException.class));
             }
@@ -258,7 +260,7 @@ public class GetHealthActionIT extends ESIntegTestCase {
         HealthStatus clusterCoordinationIndicatorStatus,
         boolean verbose
     ) throws Exception {
-        var response = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(verbose)).get();
+        var response = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(verbose, 1000)).get();
 
         assertThat(
             response.getStatus(),
@@ -294,7 +296,7 @@ public class GetHealthActionIT extends ESIntegTestCase {
     }
 
     private void testIndicator(Client client, HealthStatus ilmIndicatorStatus, boolean verbose) throws Exception {
-        var response = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(ILM_INDICATOR_NAME, verbose)).get();
+        var response = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(ILM_INDICATOR_NAME, verbose, 1000)).get();
         assertNull(response.getStatus());
         assertThat(response.getClusterName(), equalTo(new ClusterName(cluster().getClusterName())));
         assertThat(

+ 2 - 2
server/src/internalClusterTest/java/org/elasticsearch/health/HealthServiceIT.java

@@ -92,7 +92,7 @@ public class HealthServiceIT extends ESIntegTestCase {
                         throw new RuntimeException(e);
                     }
                 };
-                healthService.getHealth(internalCluster.client(node), TestHealthIndicatorService.NAME, true, listener);
+                healthService.getHealth(internalCluster.client(node), TestHealthIndicatorService.NAME, true, 1000, listener);
                 assertBusy(() -> assertThat(onResponseCalled.get(), equalTo(true)));
             }
         }
@@ -158,7 +158,7 @@ public class HealthServiceIT extends ESIntegTestCase {
         }
 
         @Override
-        public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) {
+        public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) {
             assertThat(healthInfo.diskInfoByNode().size(), equalTo(internalCluster().getNodeNames().length));
             for (DiskHealthInfo diskHealthInfo : healthInfo.diskInfoByNode().values()) {
                 assertThat(diskHealthInfo.healthStatus(), equalTo(HealthStatus.GREEN));

+ 1 - 1
server/src/internalClusterTest/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceIT.java

@@ -81,7 +81,7 @@ public class DiskHealthIndicatorServiceIT extends ESIntegTestCase {
                 throw new RuntimeException(e);
             }
         };
-        healthService.getHealth(internalCluster().client(node), DiskHealthIndicatorService.NAME, true, listener);
+        healthService.getHealth(internalCluster().client(node), DiskHealthIndicatorService.NAME, true, 1000, listener);
         assertBusy(() -> assertNotNull(resultListReference.get()));
         return resultListReference.get();
     }

+ 1 - 1
server/src/internalClusterTest/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceIT.java

@@ -67,7 +67,7 @@ public class RepositoryIntegrityHealthIndicatorServiceIT extends AbstractSnapsho
     }
 
     private void assertSnapshotRepositoryHealth(String message, Client client, HealthStatus status) {
-        var response = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(randomBoolean())).actionGet();
+        var response = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(randomBoolean(), 1000)).actionGet();
         assertThat(message, response.findIndicator(NAME).status(), equalTo(status));
     }
 

+ 1 - 1
server/src/main/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorService.java

@@ -104,7 +104,7 @@ public class StableMasterHealthIndicatorService implements HealthIndicatorServic
     }
 
     @Override
-    public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) {
+    public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) {
         CoordinationDiagnosticsService.CoordinationDiagnosticsResult coordinationDiagnosticsResult = coordinationDiagnosticsService
             .diagnoseMasterStability(verbose);
         return getHealthIndicatorResult(coordinationDiagnosticsResult, verbose);

+ 5 - 3
server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java

@@ -107,7 +107,7 @@ public class ShardsAvailabilityHealthIndicatorService implements HealthIndicator
     }
 
     @Override
-    public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) {
+    public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) {
         var state = clusterService.state();
         var shutdown = state.getMetadata().custom(NodesShutdownMetadata.TYPE, NodesShutdownMetadata.EMPTY);
         var status = new ShardAllocationStatus(state.getMetadata());
@@ -126,7 +126,7 @@ public class ShardsAvailabilityHealthIndicatorService implements HealthIndicator
             status.getSymptom(),
             status.getDetails(verbose),
             status.getImpacts(),
-            status.getDiagnosis(verbose)
+            status.getDiagnosis(verbose, maxAffectedResourcesCount)
         );
     }
 
@@ -893,9 +893,10 @@ public class ShardsAvailabilityHealthIndicatorService implements HealthIndicator
         /**
          * Returns the diagnosis for unassigned primary and replica shards.
          * @param verbose true if the diagnosis should be generated, false if they should be omitted.
+         * @param maxAffectedResourcesCount the max number of affected resources to be returned as part of the diagnosis
          * @return The diagnoses list the indicator identified. Alternatively, an empty list if none were found or verbose is false.
          */
-        public List<Diagnosis> getDiagnosis(boolean verbose) {
+        public List<Diagnosis> getDiagnosis(boolean verbose, int maxAffectedResourcesCount) {
             if (verbose) {
                 Map<Diagnosis.Definition, Set<String>> diagnosisToAffectedIndices = new HashMap<>(primaries.diagnosisDefinitions);
                 replicas.diagnosisDefinitions.forEach((diagnosisDef, indicesWithReplicasUnassigned) -> {
@@ -920,6 +921,7 @@ public class ShardsAvailabilityHealthIndicatorService implements HealthIndicator
                                         e.getValue()
                                             .stream()
                                             .sorted(indicesComparatorByPriorityAndName(clusterMetadata))
+                                            .limit(Math.min(e.getValue().size(), maxAffectedResourcesCount))
                                             .collect(Collectors.toList())
                                     )
                                 )

+ 27 - 11
server/src/main/java/org/elasticsearch/health/GetHealthAction.java

@@ -37,6 +37,8 @@ import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
 
+import static org.elasticsearch.action.ValidateActions.addValidationError;
+
 public class GetHealthAction extends ActionType<GetHealthAction.Response> {
 
     public static final GetHealthAction INSTANCE = new GetHealthAction();
@@ -146,21 +148,25 @@ public class GetHealthAction extends ActionType<GetHealthAction.Response> {
     public static class Request extends ActionRequest {
         private final String indicatorName;
         private final boolean verbose;
+        private final int size;
 
-        public Request(boolean verbose) {
-            // We never compute details if no indicator name is given because of the runtime cost:
-            this.indicatorName = null;
-            this.verbose = verbose;
+        public Request(boolean verbose, int size) {
+            this(null, verbose, size);
         }
 
-        public Request(String indicatorName, boolean verbose) {
+        public Request(String indicatorName, boolean verbose, int size) {
             this.indicatorName = indicatorName;
             this.verbose = verbose;
+            this.size = size;
         }
 
         @Override
         public ActionRequestValidationException validate() {
-            return null;
+            ActionRequestValidationException validationException = null;
+            if (size < 0) {
+                validationException = addValidationError("The size parameter must be a positive integer", validationException);
+            }
+            return validationException;
         }
 
         @Override
@@ -195,11 +201,21 @@ public class GetHealthAction extends ActionType<GetHealthAction.Response> {
         @Override
         protected void doExecute(Task task, Request request, ActionListener<Response> responseListener) {
             assert task instanceof CancellableTask;
-            healthService.getHealth(client, request.indicatorName, request.verbose, responseListener.map(healthIndicatorResults -> {
-                Response response = new Response(clusterService.getClusterName(), healthIndicatorResults, request.indicatorName == null);
-                healthApiStats.track(request.verbose, response);
-                return response;
-            }));
+            healthService.getHealth(
+                client,
+                request.indicatorName,
+                request.verbose,
+                request.size,
+                responseListener.map(healthIndicatorResults -> {
+                    Response response = new Response(
+                        clusterService.getClusterName(),
+                        healthIndicatorResults,
+                        request.indicatorName == null
+                    );
+                    healthApiStats.track(request.verbose, response);
+                    return response;
+                })
+            );
         }
     }
 }

+ 5 - 1
server/src/main/java/org/elasticsearch/health/HealthIndicatorService.java

@@ -22,7 +22,11 @@ public interface HealthIndicatorService {
 
     String name();
 
-    HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo);
+    default HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) {
+        return calculate(verbose, 1000, healthInfo);
+    }
+
+    HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo);
 
     /**
      * This method creates a HealthIndicatorResult with the given information. Note that it sorts the impacts by severity (the lower the

+ 12 - 7
server/src/main/java/org/elasticsearch/health/HealthService.java

@@ -76,20 +76,25 @@ public class HealthService {
      *
      * @param client        A client to be used to fetch the health data from the health node
      * @param indicatorName If not null, the returned results will only have this indicator
-     * @param explain       Whether to compute the details portion of the results
+     * @param verbose       Whether to compute the details portion of the results
      * @param listener      A listener to be notified of the list of all HealthIndicatorResult if indicatorName is null, or one
      *                      HealthIndicatorResult if indicatorName is not null
+     * @param maxAffectedResourcesCount The maximum number of affected resources to return per each type.
      * @throws ResourceNotFoundException if an indicator name is given and the indicator is not found
      */
     public void getHealth(
         Client client,
         @Nullable String indicatorName,
-        boolean explain,
+        boolean verbose,
+        int maxAffectedResourcesCount,
         ActionListener<List<HealthIndicatorResult>> listener
     ) {
+        if (maxAffectedResourcesCount < 0) {
+            throw new IllegalArgumentException("The max number of resources must be a positive integer");
+        }
         // Determine if cluster is stable enough to calculate health before running other indicators
         List<HealthIndicatorResult> preflightResults = preflightHealthIndicatorServices.stream()
-            .map(service -> service.calculate(explain, HealthInfo.EMPTY_HEALTH_INFO))
+            .map(service -> service.calculate(verbose, maxAffectedResourcesCount, HealthInfo.EMPTY_HEALTH_INFO))
             .toList();
 
         // If any of these are not GREEN, then we cannot obtain health from other indicators
@@ -113,7 +118,7 @@ public class HealthService {
                     ActionRunnable<List<HealthIndicatorResult>> calculateFilteredIndicatorsRunnable = calculateFilteredIndicatorsRunnable(
                         indicatorName,
                         healthInfo,
-                        explain,
+                        verbose,
                         listener
                     );
 
@@ -131,7 +136,7 @@ public class HealthService {
                     ActionRunnable<List<HealthIndicatorResult>> calculateFilteredIndicatorsRunnable = calculateFilteredIndicatorsRunnable(
                         indicatorName,
                         HealthInfo.EMPTY_HEALTH_INFO,
-                        explain,
+                        verbose,
                         listener
                     );
                     try {
@@ -150,7 +155,7 @@ public class HealthService {
                     return ActionRunnable.wrap(listener, l -> {
                         List<HealthIndicatorResult> results = Stream.concat(
                             filteredPreflightResults,
-                            filteredIndicators.map(service -> service.calculate(explain, healthInfo))
+                            filteredIndicators.map(service -> service.calculate(explain, maxAffectedResourcesCount, healthInfo))
                         ).toList();
 
                         validateResultsAndNotifyListener(indicatorName, results, l);
@@ -160,7 +165,7 @@ public class HealthService {
 
         } else {
             // Mark remaining indicators as UNKNOWN
-            HealthIndicatorDetails unknownDetails = healthUnknownReason(preflightResults, explain);
+            HealthIndicatorDetails unknownDetails = healthUnknownReason(preflightResults, verbose);
             Stream<HealthIndicatorResult> filteredIndicatorResults = filteredIndicators.map(
                 service -> generateUnknownResult(service, UNKNOWN_RESULT_SUMMARY_PREFLIGHT_FAILED, unknownDetails)
             );

+ 4 - 1
server/src/main/java/org/elasticsearch/health/RestGetHealthAction.java

@@ -23,6 +23,8 @@ public class RestGetHealthAction extends BaseRestHandler {
 
     private static final String VERBOSE_PARAM = "verbose";
 
+    private static final String SIZE_PARAM = "size";
+
     @Override
     public String getName() {
         // TODO: Existing - "cluster_health_action", "cat_health_action"
@@ -38,7 +40,8 @@ public class RestGetHealthAction extends BaseRestHandler {
     protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
         String indicatorName = request.param("indicator");
         boolean verbose = request.paramAsBoolean(VERBOSE_PARAM, true);
-        GetHealthAction.Request getHealthRequest = new GetHealthAction.Request(indicatorName, verbose);
+        int size = request.paramAsInt(SIZE_PARAM, 1000);
+        GetHealthAction.Request getHealthRequest = new GetHealthAction.Request(indicatorName, verbose, size);
         return channel -> new RestCancellableNodeClient(client, request.getHttpChannel()).execute(
             GetHealthAction.INSTANCE,
             getHealthRequest,

+ 12 - 11
server/src/main/java/org/elasticsearch/health/node/DiskHealthIndicatorService.java

@@ -41,6 +41,7 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import static org.elasticsearch.cluster.node.DiscoveryNode.DISCOVERY_NODE_COMPARATOR;
+import static org.elasticsearch.common.util.CollectionUtils.limitSize;
 import static org.elasticsearch.health.node.HealthIndicatorDisplayValues.are;
 import static org.elasticsearch.health.node.HealthIndicatorDisplayValues.getSortedUniqueValuesString;
 import static org.elasticsearch.health.node.HealthIndicatorDisplayValues.getTruncatedIndices;
@@ -68,7 +69,6 @@ public class DiskHealthIndicatorService implements HealthIndicatorService {
     private static final String IMPACT_INGEST_AT_RISK_ID = "ingest_capability_at_risk";
     private static final String IMPACT_CLUSTER_STABILITY_AT_RISK_ID = "cluster_stability_at_risk";
     private static final String IMPACT_CLUSTER_FUNCTIONALITY_UNAVAILABLE_ID = "cluster_functionality_unavailable";
-    private static final String IMPACT_DATA_NODE_WITHOUT_DISK_SPACE = "data_node_without_disk_space";
 
     private final ClusterService clusterService;
 
@@ -82,7 +82,7 @@ public class DiskHealthIndicatorService implements HealthIndicatorService {
     }
 
     @Override
-    public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) {
+    public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) {
         Map<String, DiskHealthInfo> diskHealthInfoMap = healthInfo.diskInfoByNode();
         if (diskHealthInfoMap == null || diskHealthInfoMap.isEmpty()) {
             /*
@@ -107,7 +107,7 @@ public class DiskHealthIndicatorService implements HealthIndicatorService {
             diskHealthAnalyzer.getSymptom(),
             diskHealthAnalyzer.getDetails(verbose),
             diskHealthAnalyzer.getImpacts(),
-            diskHealthAnalyzer.getDiagnoses()
+            diskHealthAnalyzer.getDiagnoses(maxAffectedResourcesCount)
         );
     }
 
@@ -344,7 +344,7 @@ public class DiskHealthIndicatorService implements HealthIndicatorService {
             return impacts;
         }
 
-        private List<Diagnosis> getDiagnoses() {
+        private List<Diagnosis> getDiagnoses(int size) {
             if (healthStatus == HealthStatus.GREEN) {
                 return List.of();
             }
@@ -353,7 +353,7 @@ public class DiskHealthIndicatorService implements HealthIndicatorService {
                 Set<String> affectedIndices = Sets.union(blockedIndices, indicesAtRisk);
                 List<Diagnosis.Resource> affectedResources = new ArrayList<>();
                 if (dataNodes.size() > 0) {
-                    Diagnosis.Resource nodeResources = new Diagnosis.Resource(dataNodes);
+                    Diagnosis.Resource nodeResources = new Diagnosis.Resource(limitSize(dataNodes, size));
                     affectedResources.add(nodeResources);
                 }
                 if (affectedIndices.size() > 0) {
@@ -361,6 +361,7 @@ public class DiskHealthIndicatorService implements HealthIndicatorService {
                         Diagnosis.Resource.Type.INDEX,
                         affectedIndices.stream()
                             .sorted(indicesComparatorByPriorityAndName(clusterState.metadata()))
+                            .limit(Math.min(affectedIndices.size(), size))
                             .collect(Collectors.toList())
                     );
                     affectedResources.add(indexResources);
@@ -405,16 +406,16 @@ public class DiskHealthIndicatorService implements HealthIndicatorService {
                 }
             }
             if (masterNodes.containsKey(HealthStatus.RED)) {
-                diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.RED, masterNodes.get(HealthStatus.RED), true));
+                diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.RED, masterNodes.get(HealthStatus.RED), size, true));
             }
             if (masterNodes.containsKey(HealthStatus.YELLOW)) {
-                diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.YELLOW, masterNodes.get(HealthStatus.YELLOW), true));
+                diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.YELLOW, masterNodes.get(HealthStatus.YELLOW), size, true));
             }
             if (otherNodes.containsKey(HealthStatus.RED)) {
-                diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.RED, otherNodes.get(HealthStatus.RED), false));
+                diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.RED, otherNodes.get(HealthStatus.RED), size, false));
             }
             if (otherNodes.containsKey(HealthStatus.YELLOW)) {
-                diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.YELLOW, otherNodes.get(HealthStatus.YELLOW), false));
+                diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.YELLOW, otherNodes.get(HealthStatus.YELLOW), size, false));
             }
             return diagnosisList;
         }
@@ -487,7 +488,7 @@ public class DiskHealthIndicatorService implements HealthIndicatorService {
                 .collect(Collectors.toSet());
         }
 
-        private Diagnosis createNonDataNodeDiagnosis(HealthStatus healthStatus, Collection<DiscoveryNode> nodes, boolean isMaster) {
+        private Diagnosis createNonDataNodeDiagnosis(HealthStatus healthStatus, List<DiscoveryNode> nodes, int size, boolean isMaster) {
             return new Diagnosis(
                 new Diagnosis.Definition(
                     NAME,
@@ -496,7 +497,7 @@ public class DiskHealthIndicatorService implements HealthIndicatorService {
                     "Please add capacity to the current nodes, or replace them with ones with higher capacity.",
                     isMaster ? "https://ela.st/fix-master-disk" : "https://ela.st/fix-disk-space"
                 ),
-                List.of(new Diagnosis.Resource(nodes))
+                List.of(new Diagnosis.Resource(limitSize(nodes, size)))
             );
         }
 

+ 7 - 2
server/src/main/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorService.java

@@ -72,7 +72,7 @@ public class RepositoryIntegrityHealthIndicatorService implements HealthIndicato
     }
 
     @Override
-    public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) {
+    public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) {
         var snapshotMetadata = clusterService.state().metadata().custom(RepositoriesMetadata.TYPE, RepositoriesMetadata.EMPTY);
 
         if (snapshotMetadata.repositories().isEmpty()) {
@@ -133,7 +133,12 @@ public class RepositoryIntegrityHealthIndicatorService implements HealthIndicato
                 )
                 : HealthIndicatorDetails.EMPTY,
             impacts,
-            List.of(new Diagnosis(CORRUPTED_REPOSITORY, List.of(new Diagnosis.Resource(Type.SNAPSHOT_REPOSITORY, corrupted))))
+            List.of(
+                new Diagnosis(
+                    CORRUPTED_REPOSITORY,
+                    List.of(new Diagnosis.Resource(Type.SNAPSHOT_REPOSITORY, limitSize(corrupted, maxAffectedResourcesCount)))
+                )
+            )
         );
     }
 

+ 106 - 36
server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java

@@ -105,7 +105,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             ),
             List.of()
         );
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         assertThat(
             service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO),
@@ -135,7 +135,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             ),
             List.of()
         );
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         assertThat(
             service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO),
@@ -175,7 +175,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             List.of(index("red-index", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), AVAILABLE))),
             List.of()
         );
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         assertThat(
             service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO),
@@ -203,7 +203,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
 
     public void testShouldBeRedWhenThereAreUnassignedPrimariesAndNoReplicas() {
         var clusterState = createClusterStateWith(List.of(index("red-index", new ShardAllocation(randomNodeId(), UNAVAILABLE))), List.of());
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         assertThat(
             service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO),
@@ -234,7 +234,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             List.of(index("red-index", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), UNAVAILABLE))),
             List.of()
         );
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         HealthIndicatorResult result = service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO);
         assertEquals(RED, result.status());
@@ -266,7 +266,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             List.of(),
             List.of()
         );
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         HealthIndicatorResult result = service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO);
         assertEquals(RED, result.status());
@@ -314,7 +314,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             List.of(),
             List.of()
         );
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         HealthIndicatorResult result = service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO);
         // index-2 has the higher priority so it ought to be listed first, followed by index-1 then index-3 which have the same priority:
@@ -344,7 +344,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             ),
             List.of(new NodeShutdown("node-0", RESTART, 60))
         );
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         assertThat(
             service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO),
@@ -365,7 +365,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             List.of(index("primaries-only-index", new ShardAllocation(randomNodeId(), AVAILABLE))),
             List.of(new NodeShutdown("node-0", RESTART, 60))
         );
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         assertThat(
             service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO),
@@ -392,7 +392,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             ),
             List.of(new NodeShutdown("node-0", RESTART, 60))
         );
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         assertThat(
             service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO),
@@ -427,7 +427,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             List.of(index("restarting-index", new ShardAllocation("node-0", INITIALIZING))),
             List.of()
         );
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         assertThat(
             service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO),
@@ -448,7 +448,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             List.of(index("restarting-index", new ShardAllocation("node-0", RESTARTING, System.nanoTime()))),
             List.of(new NodeShutdown("node-0", RESTART, 60))
         );
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         assertThat(
             service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO),
@@ -474,7 +474,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             ),
             List.of(new NodeShutdown("node-0", RESTART, 60))
         );
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         assertThat(
             service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO),
@@ -519,7 +519,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             List.of()
         );
 
-        var service = createAllocationHealthIndicatorService(clusterState);
+        var service = createShardsAvailabilityIndicatorService(clusterState);
 
         assertThat(
             service.calculate(false, HealthInfo.EMPTY_HEALTH_INFO),
@@ -555,7 +555,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             new ShardAllocation(randomNodeId(), UNAVAILABLE, noShardCopy())
         );
 
-        var service = createAllocationHealthIndicatorService();
+        var service = createShardsAvailabilityIndicatorService();
         List<Diagnosis.Definition> definitions = service.diagnoseUnassignedShardRouting(shardRouting, ClusterState.EMPTY_STATE);
 
         assertThat(definitions, hasSize(1));
@@ -601,7 +601,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
                 MoveDecision.NOT_TAKEN
             )
         );
-        var service = createAllocationHealthIndicatorService(clusterState, decisionMap);
+        var service = createShardsAvailabilityIndicatorService(clusterState, decisionMap);
 
         // Get the list of user actions that are generated for this unassigned index shard
         ShardRouting shardRouting = clusterState.routingTable().index(indexMetadata.getIndex()).shard(0).primaryShard();
@@ -624,7 +624,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             .numberOfReplicas(0)
             .build();
 
-        var service = createAllocationHealthIndicatorService();
+        var service = createShardsAvailabilityIndicatorService();
 
         // Get the list of user actions that are generated for this unassigned index shard
         List<Diagnosis.Definition> actions = service.checkIsAllocationDisabled(
@@ -652,7 +652,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             .build();
 
         // Disallow allocations in cluster settings
-        var service = createAllocationHealthIndicatorService(
+        var service = createShardsAvailabilityIndicatorService(
             Settings.builder().put(EnableAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ENABLE_SETTING.getKey(), "none").build(),
             ClusterState.EMPTY_STATE,
             Map.of()
@@ -689,7 +689,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             .build();
 
         // Disallow allocations in cluster settings
-        var service = createAllocationHealthIndicatorService(
+        var service = createShardsAvailabilityIndicatorService(
             Settings.builder().put(EnableAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ENABLE_SETTING.getKey(), "none").build(),
             ClusterState.EMPTY_STATE,
             Map.of()
@@ -726,7 +726,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             .numberOfReplicas(0)
             .build();
 
-        var service = createAllocationHealthIndicatorService();
+        var service = createShardsAvailabilityIndicatorService();
 
         // Get the list of user actions that are generated for this unassigned index shard
         List<Diagnosis.Definition> actions = service.checkDataTierRelatedIssues(
@@ -785,7 +785,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             List.of(),
             List.of(hotNode)
         );
-        var service = createAllocationHealthIndicatorService();
+        var service = createShardsAvailabilityIndicatorService();
 
         // Get the list of user actions that are generated for this unassigned index shard
         List<Diagnosis.Definition> actions = service.checkDataTierRelatedIssues(
@@ -846,7 +846,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
         );
 
         // Configure at most 1 shard per node
-        var service = createAllocationHealthIndicatorService(
+        var service = createShardsAvailabilityIndicatorService(
             Settings.builder().put(CLUSTER_TOTAL_SHARDS_PER_NODE_SETTING.getKey(), 1).build(),
             clusterState,
             Map.of()
@@ -910,7 +910,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             List.of(),
             List.of(dataNode)
         );
-        var service = createAllocationHealthIndicatorService();
+        var service = createShardsAvailabilityIndicatorService();
 
         // Get the list of user actions that are generated for this unassigned index shard
         List<Diagnosis.Definition> actions = service.checkDataTierRelatedIssues(
@@ -971,7 +971,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
         );
 
         // Configure at most 1 shard per node
-        var service = createAllocationHealthIndicatorService(
+        var service = createShardsAvailabilityIndicatorService(
             Settings.builder().put(CLUSTER_TOTAL_SHARDS_PER_NODE_SETTING.getKey(), 1).build(),
             clusterState,
             Map.of()
@@ -1010,7 +1010,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             .numberOfReplicas(0)
             .build();
 
-        var service = createAllocationHealthIndicatorService();
+        var service = createShardsAvailabilityIndicatorService();
 
         // Get the list of user actions that are generated for this unassigned index shard
         List<Diagnosis.Definition> actions = service.checkDataTierRelatedIssues(
@@ -1046,7 +1046,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             .numberOfReplicas(0)
             .build();
 
-        var service = createAllocationHealthIndicatorService();
+        var service = createShardsAvailabilityIndicatorService();
 
         // Get the list of user actions that are generated for this unassigned index shard
         List<Diagnosis.Definition> actions = service.checkDataTierRelatedIssues(
@@ -1081,7 +1081,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             .numberOfReplicas(0)
             .build();
 
-        var service = createAllocationHealthIndicatorService();
+        var service = createShardsAvailabilityIndicatorService();
 
         // Get the list of user actions that are generated for this unassigned index shard
         List<Diagnosis.Definition> actions = service.checkDataTierRelatedIssues(
@@ -1116,7 +1116,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             .numberOfReplicas(0)
             .build();
 
-        var service = createAllocationHealthIndicatorService();
+        var service = createShardsAvailabilityIndicatorService();
 
         // Get the list of user actions that are generated for this unassigned index shard
         List<Diagnosis.Definition> actions = service.checkDataTierRelatedIssues(
@@ -1156,7 +1156,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             .numberOfReplicas(0)
             .build();
 
-        var service = createAllocationHealthIndicatorService();
+        var service = createShardsAvailabilityIndicatorService();
 
         // Get the list of user actions that are generated for this unassigned index shard
         List<Diagnosis.Definition> actions = service.checkDataTierRelatedIssues(
@@ -1183,6 +1183,76 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
         assertThat(actions, contains(ACTION_INCREASE_NODE_CAPACITY));
     }
 
+    public void testLimitNumberOfAffectedResources() {
+        var clusterState = createClusterStateWith(
+            List.of(
+                index("red-index1", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), AVAILABLE)),
+                index("red-index2", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), AVAILABLE)),
+                index("red-index3", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), AVAILABLE)),
+                index("red-index4", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), AVAILABLE)),
+                index("red-index5", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), AVAILABLE))
+            ),
+            List.of()
+        );
+        var service = createShardsAvailabilityIndicatorService(clusterState);
+
+        {
+            // assert the full result to check that details, impacts, and symptoms use the correct count of affected indices (5)
+            assertThat(
+                service.calculate(true, 2, HealthInfo.EMPTY_HEALTH_INFO),
+                equalTo(
+                    createExpectedResult(
+                        RED,
+                        "This cluster has 5 unavailable primary shards.",
+                        Map.of("unassigned_primaries", 5, "started_replicas", 5),
+                        List.of(
+                            new HealthIndicatorImpact(
+                                NAME,
+                                ShardsAvailabilityHealthIndicatorService.PRIMARY_UNASSIGNED_IMPACT_ID,
+                                1,
+                                "Cannot add data to 5 indices [red-index1, red-index2, red-index3, red-index4, red-index5]. Searches might "
+                                    + "return incomplete results.",
+                                List.of(ImpactArea.INGEST, ImpactArea.SEARCH)
+                            )
+                        ),
+                        List.of(
+                            new Diagnosis(
+                                ACTION_CHECK_ALLOCATION_EXPLAIN_API,
+                                List.of(new Diagnosis.Resource(INDEX, List.of("red-index1", "red-index2")))
+                            )
+                        )
+                    )
+                )
+            );
+        }
+
+        {
+            // larger number of affected resources
+            assertThat(
+                service.calculate(true, 2_000, HealthInfo.EMPTY_HEALTH_INFO).diagnosisList(),
+                equalTo(
+                    List.of(
+                        new Diagnosis(
+                            ACTION_CHECK_ALLOCATION_EXPLAIN_API,
+                            List.of(
+                                new Diagnosis.Resource(INDEX, List.of("red-index1", "red-index2", "red-index3", "red-index4", "red-index5"))
+                            )
+                        )
+                    )
+                )
+            );
+
+        }
+
+        {
+            // 0 affected resources
+            assertThat(
+                service.calculate(true, 0, HealthInfo.EMPTY_HEALTH_INFO).diagnosisList(),
+                equalTo(List.of(new Diagnosis(ACTION_CHECK_ALLOCATION_EXPLAIN_API, List.of(new Diagnosis.Resource(INDEX, List.of())))))
+            );
+        }
+    }
+
     private HealthIndicatorResult createExpectedResult(
         HealthStatus status,
         String symptom,
@@ -1452,22 +1522,22 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
 
     private record ShardRoutingKey(String index, int shard, boolean primary) {}
 
-    private static ShardsAvailabilityHealthIndicatorService createAllocationHealthIndicatorService() {
-        return createAllocationHealthIndicatorService(ClusterState.EMPTY_STATE, Collections.emptyMap());
+    private static ShardsAvailabilityHealthIndicatorService createShardsAvailabilityIndicatorService() {
+        return createShardsAvailabilityIndicatorService(ClusterState.EMPTY_STATE, Collections.emptyMap());
     }
 
-    private static ShardsAvailabilityHealthIndicatorService createAllocationHealthIndicatorService(ClusterState clusterState) {
-        return createAllocationHealthIndicatorService(clusterState, Collections.emptyMap());
+    private static ShardsAvailabilityHealthIndicatorService createShardsAvailabilityIndicatorService(ClusterState clusterState) {
+        return createShardsAvailabilityIndicatorService(clusterState, Collections.emptyMap());
     }
 
-    private static ShardsAvailabilityHealthIndicatorService createAllocationHealthIndicatorService(
+    private static ShardsAvailabilityHealthIndicatorService createShardsAvailabilityIndicatorService(
         ClusterState clusterState,
         final Map<ShardRoutingKey, ShardAllocationDecision> decisions
     ) {
-        return createAllocationHealthIndicatorService(Settings.EMPTY, clusterState, decisions);
+        return createShardsAvailabilityIndicatorService(Settings.EMPTY, clusterState, decisions);
     }
 
-    private static ShardsAvailabilityHealthIndicatorService createAllocationHealthIndicatorService(
+    private static ShardsAvailabilityHealthIndicatorService createShardsAvailabilityIndicatorService(
         Settings nodeSettings,
         ClusterState clusterState,
         final Map<ShardRoutingKey, ShardAllocationDecision> decisions

+ 27 - 0
server/src/test/java/org/elasticsearch/health/GetHealthRequestTests.java

@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.health;
+
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.test.ESTestCase;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+public class GetHealthRequestTests extends ESTestCase {
+
+    public void testValidation() {
+        var req = new GetHealthAction.Request(true, -1);
+        ActionRequestValidationException validationException = req.validate();
+        assertThat(validationException, notNullValue());
+        assertThat(validationException.validationErrors().size(), is(1));
+        assertThat(validationException.validationErrors().get(0), containsString("The size parameter must be a positive integer"));
+    }
+}

+ 1 - 1
server/src/test/java/org/elasticsearch/health/HealthIndicatorServiceTests.java

@@ -90,7 +90,7 @@ public class HealthIndicatorServiceTests extends ESTestCase {
             }
 
             @Override
-            public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) {
+            public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) {
                 return null;
             }
         };

+ 18 - 4
server/src/test/java/org/elasticsearch/health/HealthServiceTests.java

@@ -88,6 +88,7 @@ public class HealthServiceTests extends ESTestCase {
             client,
             indicatorName,
             false,
+            1000,
             getExpectedHealthIndicatorResultsActionListener(onResponseCalled, expectedHealthIndicatorResults)
         );
         assertBusy(() -> assertThat(onResponseCalled.get(), equalTo(true)));
@@ -138,11 +139,24 @@ public class HealthServiceTests extends ESTestCase {
         );
     }
 
+    @SuppressWarnings("unchecked")
+    public void testValidateSize() {
+        var shardsAvailable = new HealthIndicatorResult("shards_availability", GREEN, null, null, null, null);
+
+        var service = new HealthService(Collections.emptyList(), List.of(createMockHealthIndicatorService(shardsAvailable)), threadPool);
+        NodeClient client = getTestClient(HealthInfo.EMPTY_HEALTH_INFO);
+        IllegalArgumentException illegalArgumentException = expectThrows(
+            IllegalArgumentException.class,
+            () -> service.getHealth(client, null, true, -1, ActionListener.NOOP)
+        );
+        assertThat(illegalArgumentException.getMessage(), is("The max number of resources must be a positive integer"));
+    }
+
     private <T extends Throwable> void assertGetHealthThrowsException(
         HealthService service,
         NodeClient client,
         String indicatorName,
-        boolean explain,
+        boolean verbose,
         Class<T> expectedType,
         String expectedMessage,
         boolean expectOnFailCalled
@@ -154,7 +168,7 @@ public class HealthServiceTests extends ESTestCase {
             expectedMessage
         );
         try {
-            service.getHealth(client, indicatorName, explain, listener);
+            service.getHealth(client, indicatorName, verbose, 1000, listener);
         } catch (Throwable t) {
             if (expectOnFailCalled || (expectedType.isInstance(t) == false)) {
                 throw new RuntimeException("Unexpected throwable", t);
@@ -325,7 +339,7 @@ public class HealthServiceTests extends ESTestCase {
                 throw new RuntimeException(e);
             }
         };
-        service.getHealth(client, indicatorName, false, listener);
+        service.getHealth(client, indicatorName, false, 1000, listener);
         assertBusy(() -> assertNotNull(resultReference.get()));
         return resultReference.get();
     }
@@ -366,7 +380,7 @@ public class HealthServiceTests extends ESTestCase {
             }
 
             @Override
-            public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) {
+            public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) {
                 if (expectedHealthInfo != null) {
                     assertThat(healthInfo, equalTo(expectedHealthInfo));
                 }

+ 81 - 0
server/src/test/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceTests.java

@@ -882,6 +882,87 @@ public class DiskHealthIndicatorServiceTests extends ESTestCase {
         }
     }
 
+    public void testLimitNumberOfAffectedResources() {
+        Set<DiscoveryNodeRole> otherRoles = new HashSet<>(randomNonEmptySubsetOf(OTHER_ROLES));
+        Set<DiscoveryNodeRole> dataRoles = new HashSet<>(randomNonEmptySubsetOf(DATA_ROLES));
+        Set<DiscoveryNodeRole> masterRole = Set.of(DiscoveryNodeRole.MASTER_ROLE);
+        Set<DiscoveryNode> dataNodes = createNodes(30, dataRoles);
+        Set<DiscoveryNode> masterNodes = createNodes(20, masterRole);
+        Set<DiscoveryNode> otherNodes = createNodes(10, otherRoles);
+        ClusterService clusterService = createClusterService(Sets.union(Sets.union(dataNodes, masterNodes), otherNodes), true);
+        DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService);
+        int numberOfRedMasterNodes = masterNodes.size();
+        int numberOfRedOtherNodes = otherNodes.size();
+        int numberOfYellowDataNodes = dataNodes.size();
+        HealthInfo healthInfo = createHealthInfo(
+            List.of(
+                new HealthInfoConfig(HealthStatus.YELLOW, numberOfYellowDataNodes, dataNodes),
+                new HealthInfoConfig(HealthStatus.RED, numberOfRedMasterNodes, masterNodes),
+                new HealthInfoConfig(HealthStatus.RED, numberOfRedOtherNodes, otherNodes)
+            )
+        );
+        {
+            HealthIndicatorResult result = diskHealthIndicatorService.calculate(true, 0, healthInfo);
+            List<Diagnosis> diagnosisList = result.diagnosisList();
+            assertThat(diagnosisList.size(), equalTo(3));
+            {
+                Diagnosis diagnosis = diagnosisList.get(0);
+                List<Diagnosis.Resource> dataAffectedResources = diagnosis.affectedResources();
+                assertThat(dataAffectedResources.size(), equalTo(2));
+                assertThat(dataAffectedResources.get(0).getType(), is(Diagnosis.Resource.Type.NODE));
+                assertThat(dataAffectedResources.get(0).getNodes().size(), is(0));
+                assertThat(dataAffectedResources.get(1).getType(), is(Diagnosis.Resource.Type.INDEX));
+                assertThat(dataAffectedResources.get(1).getValues().size(), is(0));
+            }
+            {
+                Diagnosis diagnosis = diagnosisList.get(1);
+                List<Diagnosis.Resource> masterAffectedResources = diagnosis.affectedResources();
+                assertThat(masterAffectedResources.size(), equalTo(1));
+                assertThat(masterAffectedResources.get(0).getType(), is(Diagnosis.Resource.Type.NODE));
+                assertThat(masterAffectedResources.get(0).getNodes().size(), is(0));
+            }
+
+            {
+                Diagnosis diagnosis = diagnosisList.get(2);
+                List<Diagnosis.Resource> nonDataNonMasterAffectedResources = diagnosis.affectedResources();
+                assertThat(nonDataNonMasterAffectedResources.size(), equalTo(1));
+                assertThat(nonDataNonMasterAffectedResources.get(0).getType(), is(Diagnosis.Resource.Type.NODE));
+                assertThat(nonDataNonMasterAffectedResources.get(0).getNodes().size(), is(0));
+            }
+        }
+
+        {
+            HealthIndicatorResult result = diskHealthIndicatorService.calculate(true, 10, healthInfo);
+            List<Diagnosis> diagnosisList = result.diagnosisList();
+            assertThat(diagnosisList.size(), equalTo(3));
+            {
+                Diagnosis diagnosis = diagnosisList.get(0);
+                List<Diagnosis.Resource> dataAffectedResources = diagnosis.affectedResources();
+                assertThat(dataAffectedResources.size(), equalTo(2));
+                assertThat(dataAffectedResources.get(0).getType(), is(Diagnosis.Resource.Type.NODE));
+                assertThat(dataAffectedResources.get(0).getNodes().size(), is(10));
+                assertThat(dataAffectedResources.get(1).getType(), is(Diagnosis.Resource.Type.INDEX));
+                assertThat(dataAffectedResources.get(1).getValues().size(), is(1));
+            }
+            {
+                Diagnosis diagnosis = diagnosisList.get(1);
+                List<Diagnosis.Resource> masterAffectedResources = diagnosis.affectedResources();
+                assertThat(masterAffectedResources.size(), equalTo(1));
+                assertThat(masterAffectedResources.get(0).getType(), is(Diagnosis.Resource.Type.NODE));
+                assertThat(masterAffectedResources.get(0).getNodes().size(), is(10));
+            }
+
+            {
+                Diagnosis diagnosis = diagnosisList.get(2);
+                List<Diagnosis.Resource> nonDataNonMasterAffectedResources = diagnosis.affectedResources();
+                assertThat(nonDataNonMasterAffectedResources.size(), equalTo(1));
+                assertThat(nonDataNonMasterAffectedResources.get(0).getType(), is(Diagnosis.Resource.Type.NODE));
+                assertThat(nonDataNonMasterAffectedResources.get(0).getNodes().size(), is(10));
+            }
+        }
+
+    }
+
     private Set<DiscoveryNode> createNodesWithAllRoles() {
         return createNodes(DiscoveryNodeRole.roles());
     }

+ 37 - 0
server/src/test/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceTests.java

@@ -28,6 +28,7 @@ import org.elasticsearch.test.ESTestCase;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Stream;
 
 import static org.elasticsearch.common.util.CollectionUtils.appendToCopy;
 import static org.elasticsearch.health.HealthStatus.GREEN;
@@ -115,6 +116,42 @@ public class RepositoryIntegrityHealthIndicatorServiceTests extends ESTestCase {
         );
     }
 
+    public void testLimitNumberOfAffectedResources() {
+        List<RepositoryMetadata> repos = Stream.iterate(0, n -> n + 1)
+            .limit(20)
+            .map(i -> createRepositoryMetadata("corrupted-repo" + i, true))
+            .toList();
+        var clusterState = createClusterStateWith(new RepositoriesMetadata(repos));
+        var service = createRepositoryCorruptionHealthIndicatorService(clusterState);
+
+        {
+            assertThat(
+                service.calculate(true, 10, HealthInfo.EMPTY_HEALTH_INFO).diagnosisList(),
+                equalTo(
+                    List.of(
+                        new Diagnosis(
+                            CORRUPTED_REPOSITORY,
+                            List.of(
+                                new Diagnosis.Resource(
+                                    Type.SNAPSHOT_REPOSITORY,
+                                    repos.stream().limit(10).map(RepositoryMetadata::name).toList()
+                                )
+                            )
+                        )
+                    )
+                )
+            );
+        }
+
+        {
+            assertThat(
+                service.calculate(true, 0, HealthInfo.EMPTY_HEALTH_INFO).diagnosisList(),
+                equalTo(List.of(new Diagnosis(CORRUPTED_REPOSITORY, List.of(new Diagnosis.Resource(Type.SNAPSHOT_REPOSITORY, List.of())))))
+            );
+        }
+
+    }
+
     private static ClusterState createClusterStateWith(RepositoriesMetadata metadata) {
         var builder = ClusterState.builder(new ClusterName("test-cluster"));
         if (metadata != null) {

+ 3 - 3
x-pack/plugin/core/src/internalClusterTest/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierShardAvailabilityHealthIndicatorIT.java

@@ -70,7 +70,7 @@ public class DataTierShardAvailabilityHealthIndicatorIT extends ESIntegTestCase
         ensureYellow("test");
         GetHealthAction.Response healthResponse = client().execute(
             GetHealthAction.INSTANCE,
-            new GetHealthAction.Request(ShardsAvailabilityHealthIndicatorService.NAME, true)
+            new GetHealthAction.Request(ShardsAvailabilityHealthIndicatorService.NAME, true, 1000)
         ).get();
         HealthIndicatorResult indicatorResult = healthResponse.findIndicator(ShardsAvailabilityHealthIndicatorService.NAME);
         assertThat(indicatorResult.status(), equalTo(HealthStatus.YELLOW));
@@ -107,7 +107,7 @@ public class DataTierShardAvailabilityHealthIndicatorIT extends ESIntegTestCase
         ensureYellow("test");
         GetHealthAction.Response healthResponse = client().execute(
             GetHealthAction.INSTANCE,
-            new GetHealthAction.Request(ShardsAvailabilityHealthIndicatorService.NAME, true)
+            new GetHealthAction.Request(ShardsAvailabilityHealthIndicatorService.NAME, true, 1000)
         ).get();
         ClusterAllocationExplanation explain = client().admin()
             .cluster()
@@ -152,7 +152,7 @@ public class DataTierShardAvailabilityHealthIndicatorIT extends ESIntegTestCase
         ensureYellow("test");
         GetHealthAction.Response healthResponse = client().execute(
             GetHealthAction.INSTANCE,
-            new GetHealthAction.Request(ShardsAvailabilityHealthIndicatorService.NAME, true)
+            new GetHealthAction.Request(ShardsAvailabilityHealthIndicatorService.NAME, true, 1000)
         ).get();
         HealthIndicatorResult indicatorResult = healthResponse.findIndicator(ShardsAvailabilityHealthIndicatorService.NAME);
         assertThat(indicatorResult.status(), equalTo(HealthStatus.YELLOW));

+ 1 - 1
x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java

@@ -66,7 +66,7 @@ public class IlmHealthIndicatorService implements HealthIndicatorService {
     }
 
     @Override
-    public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) {
+    public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) {
         final ClusterState currentState = clusterService.state();
         var ilmMetadata = currentState.metadata().custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY);
         final OperationMode currentMode = currentILMMode(currentState);

+ 5 - 2
x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java

@@ -99,7 +99,7 @@ public class SlmHealthIndicatorService implements HealthIndicatorService {
     }
 
     @Override
-    public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) {
+    public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) {
         final ClusterState currentState = clusterService.state();
         var slmMetadata = currentState.metadata().custom(SnapshotLifecycleMetadata.TYPE, SnapshotLifecycleMetadata.EMPTY);
         final OperationMode currentMode = currentSLMMode(currentState);
@@ -181,7 +181,10 @@ public class SlmHealthIndicatorService implements HealthIndicatorService {
                             List.of(
                                 new Diagnosis.Resource(
                                     Diagnosis.Resource.Type.SLM_POLICY,
-                                    unhealthyPolicies.stream().map(SnapshotLifecyclePolicyMetadata::getName).toList()
+                                    unhealthyPolicies.stream()
+                                        .map(SnapshotLifecyclePolicyMetadata::getName)
+                                        .limit(Math.min(unhealthyPolicies.size(), maxAffectedResourcesCount))
+                                        .toList()
                                 )
                             )
                         )