diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/ClusterHealth.java b/server/sonar-server/src/main/java/org/sonar/server/health/ClusterHealth.java new file mode 100644 index 000000000000..593a1d2d7a8a --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/ClusterHealth.java @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.health; + +import java.util.Objects; +import java.util.Set; +import org.sonar.cluster.health.NodeHealth; + +import static com.google.common.collect.ImmutableSet.copyOf; +import static java.util.Objects.requireNonNull; + +public class ClusterHealth { + private final Health health; + private final Set nodes; + + public ClusterHealth(Health health, Set nodes) { + this.health = requireNonNull(health, "health can't be null"); + this.nodes = copyOf(requireNonNull(nodes, "nodes can't be null")); + } + + public Health getHealth() { + return health; + } + + public Set getNodes() { + return nodes; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClusterHealth that = (ClusterHealth) o; + return Objects.equals(health, that.health) && + Objects.equals(nodes, that.nodes); + } + + @Override + public int hashCode() { + return Objects.hash(health, nodes); + } + + @Override + public String toString() { + return "ClusterHealth{" + + "health=" + health + + ", nodes=" + nodes + + '}'; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/Health.java b/server/sonar-server/src/main/java/org/sonar/server/health/Health.java index 065b64a5771a..4f2136dfd1cd 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/health/Health.java +++ b/server/sonar-server/src/main/java/org/sonar/server/health/Health.java @@ -31,7 +31,7 @@ public class Health { /** * The GREEN status without any cause as a constant, for convenience and optimisation. */ - static final Health GREEN = newHealthCheckBuilder() + public static final Health GREEN = newHealthCheckBuilder() .setStatus(Status.GREEN) .build(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/HealthChecker.java b/server/sonar-server/src/main/java/org/sonar/server/health/HealthChecker.java index f455e97d52b1..a205b808a39b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/health/HealthChecker.java +++ b/server/sonar-server/src/main/java/org/sonar/server/health/HealthChecker.java @@ -31,5 +31,5 @@ public interface HealthChecker { * * @throws IllegalStateException if clustering is not enabled. */ - Health checkCluster(); + ClusterHealth checkCluster(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/HealthCheckerImpl.java b/server/sonar-server/src/main/java/org/sonar/server/health/HealthCheckerImpl.java index 630e308c06f8..fbaabfe4f18a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/health/HealthCheckerImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/health/HealthCheckerImpl.java @@ -70,14 +70,15 @@ public Health checkNode() { } @Override - public Health checkCluster() { + public ClusterHealth checkCluster() { checkState(!webServer.isStandalone(), "Clustering is not enabled"); checkState(sharedHealthState != null, "HealthState instance can't be null when clustering is enabled"); Set nodeHealths = sharedHealthState.readAll(); - return clusterHealthChecks.stream() + Health health = clusterHealthChecks.stream() .map(clusterHealthCheck -> clusterHealthCheck.check(nodeHealths)) .reduce(Health.GREEN, HealthReducer.INSTANCE); + return new ClusterHealth(health, nodeHealths); } private enum HealthReducer implements BinaryOperator { diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthAction.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthAction.java index f94cd0c40eb3..b4931a745cb7 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthAction.java @@ -19,47 +19,33 @@ */ package org.sonar.server.platform.ws; -import com.google.common.io.Resources; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; -import org.sonar.server.health.Health; -import org.sonar.server.health.HealthChecker; +import org.sonar.server.platform.WebServer; import org.sonar.server.ws.WsUtils; -import org.sonarqube.ws.WsSystem; public class HealthAction implements SystemWsAction { - private final HealthChecker healthChecker; + private final WebServer webServer; + private final HealthActionSupport support; - public HealthAction(HealthChecker healthChecker) { - this.healthChecker = healthChecker; + public HealthAction(WebServer webServer, HealthActionSupport support) { + this.webServer = webServer; + this.support = support; } @Override public void define(WebService.NewController controller) { - controller.createAction("health") - .setDescription("Provide health status of the current SonarQube instance." + - "

status: the health status" + - "

    " + - "
  • GREEN: SonarQube is fully operational
  • " + - "
  • YELLOW: SonarQube is operational but something must be fixed to be fully operational
  • " + - "
  • RED: SonarQube is not operational
  • " + - "
" + - "

") - .setSince("6.6") - .setResponseExample(Resources.getResource(this.getClass(), "example-health.json")) - .setHandler(this); + support.define(controller, this); } @Override public void handle(Request request, Response response) throws Exception { - Health check = healthChecker.checkNode(); - WsSystem.HealthResponse.Builder responseBuilder = WsSystem.HealthResponse.newBuilder() - .setHealth(WsSystem.Health.valueOf(check.getStatus().name())); - WsSystem.Cause.Builder causeBuilder = WsSystem.Cause.newBuilder(); - check.getCauses().forEach(str -> responseBuilder.addCauses(causeBuilder.clear().setMessage(str).build())); - - WsUtils.writeProtobuf(responseBuilder.build(), request, response); + if (webServer.isStandalone()) { + WsUtils.writeProtobuf(support.checkNodeHealth(), request, response); + } else { + WsUtils.writeProtobuf(support.checkClusterHealth(), request, response); + } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java index fe7f38efd12e..f73abfe4591a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java @@ -41,6 +41,7 @@ protected void configureModule() { add(EsStatusClusterCheck.class); add(HealthCheckerImpl.class, + HealthActionSupport.class, HealthAction.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionSupport.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionSupport.java new file mode 100644 index 000000000000..9df0678a9fc0 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionSupport.java @@ -0,0 +1,113 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.ws; + +import com.google.common.io.Resources; +import java.util.Comparator; +import org.sonar.api.server.ws.WebService; +import org.sonar.cluster.health.NodeDetails; +import org.sonar.cluster.health.NodeHealth; +import org.sonar.server.health.ClusterHealth; +import org.sonar.server.health.Health; +import org.sonar.server.health.HealthChecker; +import org.sonarqube.ws.WsSystem; + +import static java.lang.String.valueOf; +import static org.sonar.api.utils.DateUtils.formatDateTime; + +public class HealthActionSupport { + private static final Comparator NODE_HEALTH_COMPARATOR = Comparator.comparingInt(s -> s.getDetails().getType().ordinal()) + .thenComparing(a -> a.getDetails().getName()) + .thenComparing(a -> a.getDetails().getHost()) + .thenComparing(a -> a.getDetails().getPort()); + private final HealthChecker healthChecker; + + public HealthActionSupport(HealthChecker healthChecker) { + this.healthChecker = healthChecker; + } + + void define(WebService.NewController controller, SystemWsAction handler) { + controller.createAction("health") + .setDescription("Provide health status of SonarQube." + + "

Require 'Administer System' permission or authentication with passcode

" + + "

" + + "

    " + + "
  • GREEN: SonarQube is fully operational
  • " + + "
  • YELLOW: SonarQube is usable, but it needs attention in order to be fully operational
  • " + + "
  • RED: SonarQube is not operational
  • " + + "
" + + "

") + .setSince("6.6") + .setResponseExample(Resources.getResource(this.getClass(), "example-health.json")) + .setHandler(handler); + } + + WsSystem.HealthResponse checkNodeHealth() { + Health check = healthChecker.checkNode(); + WsSystem.HealthResponse.Builder responseBuilder = WsSystem.HealthResponse.newBuilder() + .setHealth(WsSystem.Health.valueOf(check.getStatus().name())); + WsSystem.Cause.Builder causeBuilder = WsSystem.Cause.newBuilder(); + check.getCauses().forEach(str -> responseBuilder.addCauses(causeBuilder.clear().setMessage(str).build())); + + return responseBuilder.build(); + } + + WsSystem.HealthResponse checkClusterHealth() { + ClusterHealth check = healthChecker.checkCluster(); + return toResponse(check); + } + + private static WsSystem.HealthResponse toResponse(ClusterHealth check) { + WsSystem.HealthResponse.Builder responseBuilder = WsSystem.HealthResponse.newBuilder(); + WsSystem.Node.Builder nodeBuilder = WsSystem.Node.newBuilder(); + WsSystem.Cause.Builder causeBuilder = WsSystem.Cause.newBuilder(); + + Health health = check.getHealth(); + responseBuilder.setHealth(WsSystem.Health.valueOf(health.getStatus().name())); + health.getCauses().forEach(str -> responseBuilder.addCauses(toCause(str, causeBuilder))); + + WsSystem.Nodes.Builder nodesBuilder = WsSystem.Nodes.newBuilder(); + check.getNodes().stream() + .sorted(NODE_HEALTH_COMPARATOR) + .map(node -> toNode(node, nodeBuilder, causeBuilder)) + .forEach(nodesBuilder::addNodes); + responseBuilder.setNodes(nodesBuilder.build()); + + return responseBuilder.build(); + } + + private static WsSystem.Node toNode(NodeHealth nodeHealth, WsSystem.Node.Builder nodeBuilder, WsSystem.Cause.Builder causeBuilder) { + nodeBuilder.clear(); + nodeBuilder.setHealth(WsSystem.Health.valueOf(nodeHealth.getStatus().name())); + nodeHealth.getCauses().forEach(str -> nodeBuilder.addCauses(toCause(str, causeBuilder))); + NodeDetails details = nodeHealth.getDetails(); + nodeBuilder + .setType(WsSystem.NodeType.valueOf(details.getType().name())) + .setName(details.getName()) + .setHost(details.getHost()) + .setPort(valueOf(details.getPort())) + .setStarted(formatDateTime(details.getStarted())); + return nodeBuilder.build(); + } + + private static WsSystem.Cause toCause(String str, WsSystem.Cause.Builder causeBuilder) { + return causeBuilder.clear().setMessage(str).build(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthAction.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthAction.java new file mode 100644 index 000000000000..1c7f953354af --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthAction.java @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.ws; + +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.server.ws.WsUtils; + +public class SafeModeHealthAction implements SystemWsAction { + private final HealthActionSupport support; + + public SafeModeHealthAction(HealthActionSupport support) { + this.support = support; + } + + @Override + public void define(WebService.NewController controller) { + support.define(controller, this); + } + + @Override + public void handle(Request request, Response response) throws Exception { + WsUtils.writeProtobuf(support.checkNodeHealth(), request, response); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthActionModule.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthActionModule.java index 364fb802eba9..cf48674fe9e4 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthActionModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthActionModule.java @@ -35,6 +35,7 @@ protected void configureModule() { EsStatusNodeCheck.class, HealthCheckerImpl.class, - HealthAction.class); + HealthActionSupport.class, + SafeModeHealthAction.class); } } diff --git a/server/sonar-server/src/main/resources/org/sonar/server/platform/ws/example-health.json b/server/sonar-server/src/main/resources/org/sonar/server/platform/ws/example-health.json index 9d95994a7b48..b1e98f92032d 100644 --- a/server/sonar-server/src/main/resources/org/sonar/server/platform/ws/example-health.json +++ b/server/sonar-server/src/main/resources/org/sonar/server/platform/ws/example-health.json @@ -1,8 +1,63 @@ { - "health": "YELLOW", + "health": "RED", "causes": [ { - "message": "Elasticsearch status is YELLOW" + "message": "Application node app-1 is RED" + } + ], + "nodes": [ + { + "name": "app-1", + "type": "APPLICATION", + "host": "192.168.1.1", + "port": "999", + "started": "2015-08-13T23:34:59+0200", + "health": "RED", + "causes": [ + { + "message": "foo" + } + ] + }, + { + "name": "app-2", + "type": "APPLICATION", + "host": "192.168.1.2", + "port": "999", + "started": "2015-08-13T23:34:59+0200", + "health": "YELLOW", + "causes": [ + { + "message": "bar" + } + ] + }, + { + "name": "es-1", + "type": "SEARCH", + "host": "192.168.1.3", + "port": "999", + "started": "2015-08-13T23:34:59+0200", + "health": "GREEN", + "causes": [] + }, + { + "name": "es-2", + "type": "SEARCH", + "host": "192.168.1.4", + "port": "999", + "started": "2015-08-13T23:34:59+0200", + "health": "GREEN", + "causes": [] + }, + { + "name": "es-3", + "type": "SEARCH", + "host": "192.168.1.5", + "port": "999", + "started": "2015-08-13T23:34:59+0200", + "health": "GREEN", + "causes": [] } ] } diff --git a/server/sonar-server/src/test/java/org/sonar/server/health/ClusterHealthTest.java b/server/sonar-server/src/test/java/org/sonar/server/health/ClusterHealthTest.java new file mode 100644 index 000000000000..47d215e6ee21 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/health/ClusterHealthTest.java @@ -0,0 +1,124 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.health; + +import java.util.Collections; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.cluster.health.NodeDetails; +import org.sonar.cluster.health.NodeHealth; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; + +public class ClusterHealthTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private final Random random = new Random(); + + @Test + public void constructor_fails_with_NPE_if_Health_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("health can't be null"); + + new ClusterHealth(null, Collections.emptySet()); + } + + @Test + public void constructor_fails_with_NPE_if_NodeHealth_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("nodes can't be null"); + + new ClusterHealth(Health.GREEN, null); + } + + @Test + public void verify_getters() { + Health health = randomHealth(); + Set nodeHealths = randomNodeHealths(); + ClusterHealth underTest = new ClusterHealth(health, nodeHealths); + + assertThat(underTest.getHealth()).isSameAs(health); + assertThat(underTest.getNodes()).isEqualTo(nodeHealths); + } + + @Test + public void equals_is_based_on_content() { + Health health = randomHealth(); + Set nodeHealths = randomNodeHealths(); + ClusterHealth underTest = new ClusterHealth(health, nodeHealths); + + assertThat(underTest) + .isEqualTo(underTest) + .isEqualTo(new ClusterHealth(health, nodeHealths)) + .isNotEqualTo(new Object()) + .isNotEqualTo(null) + .isNotEqualTo(new ClusterHealth(randomHealth(), randomNodeHealths())); + } + + @Test + public void hashcode_is_based_on_content() { + Health health = randomHealth(); + Set nodeHealths = randomNodeHealths(); + ClusterHealth underTest = new ClusterHealth(health, nodeHealths); + + assertThat(underTest.hashCode()) + .isEqualTo(underTest.hashCode()) + .isNotEqualTo(new ClusterHealth(randomHealth(), randomNodeHealths()).hashCode()); + } + + @Test + public void verify_toString() { + Health health = randomHealth(); + Set nodeHealths = randomNodeHealths(); + + ClusterHealth underTest = new ClusterHealth(health, nodeHealths); + + assertThat(underTest.toString()).isEqualTo("ClusterHealth{health=" + health + ", nodes=" + nodeHealths + "}"); + } + + private Health randomHealth() { + Health.Builder healthBuilder = Health.newHealthCheckBuilder(); + healthBuilder.setStatus(Health.Status.values()[random.nextInt(Health.Status.values().length)]); + IntStream.range(0, random.nextInt(3)).mapToObj(i -> randomAlphanumeric(3)).forEach(healthBuilder::addCause); + return healthBuilder.build(); + } + + private Set randomNodeHealths() { + return IntStream.range(0, random.nextInt(4)).mapToObj(i -> NodeHealth.newNodeHealthBuilder() + .setStatus(NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)]) + .setDate(1 + random.nextInt(951)) + .setDetails( + NodeDetails.newNodeDetailsBuilder() + .setType(random.nextBoolean() ? NodeDetails.Type.SEARCH : NodeDetails.Type.APPLICATION) + .setName(randomAlphanumeric(3)) + .setHost(randomAlphanumeric(4)) + .setPort(1 + random.nextInt(344)) + .setStarted(1 + random.nextInt(999)) + .build()) + .build()).collect(Collectors.toSet()); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/health/HealthCheckerImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/health/HealthCheckerImplTest.java index 43e0db25c305..2fa4d352cfa5 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/health/HealthCheckerImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/health/HealthCheckerImplTest.java @@ -145,7 +145,7 @@ public void checkCluster_returns_GREEN_when_there_is_no_ClusterHealthCheck() { when(webServer.isStandalone()).thenReturn(false); HealthCheckerImpl underTest = new HealthCheckerImpl(webServer, new NodeHealthCheck[0], new ClusterHealthCheck[0], sharedHealthState); - assertThat(underTest.checkCluster()).isEqualTo(Health.GREEN); + assertThat(underTest.checkCluster().getHealth()).isEqualTo(Health.GREEN); } @Test @@ -154,7 +154,7 @@ public void checkCluster_returns_GREEN_status_if_only_GREEN_statuses_returned_by List statuses = IntStream.range(1, 1 + random.nextInt(20)).mapToObj(i -> GREEN).collect(Collectors.toList()); HealthCheckerImpl underTest = newClusterHealthCheckerImpl(statuses.stream()); - assertThat(underTest.checkCluster().getStatus()) + assertThat(underTest.checkCluster().getHealth().getStatus()) .describedAs("%s should have been computed from %s statuses", GREEN, statuses) .isEqualTo(GREEN); } @@ -169,7 +169,7 @@ public void checkCluster_returns_YELLOW_status_if_only_GREEN_and_at_least_one_YE Collections.shuffle(statuses); HealthCheckerImpl underTest = newClusterHealthCheckerImpl(statuses.stream()); - assertThat(underTest.checkCluster().getStatus()) + assertThat(underTest.checkCluster().getHealth().getStatus()) .describedAs("%s should have been computed from %s statuses", YELLOW, statuses) .isEqualTo(YELLOW); } @@ -187,7 +187,7 @@ public void checkCluster_returns_RED_status_if_at_least_one_RED_status_returned_ Collections.shuffle(statuses); HealthCheckerImpl underTest = newClusterHealthCheckerImpl(statuses.stream()); - assertThat(underTest.checkCluster().getStatus()) + assertThat(underTest.checkCluster().getHealth().getStatus()) .describedAs("%s should have been computed from %s statuses", RED, statuses) .isEqualTo(RED); } @@ -206,7 +206,7 @@ public void checkCluster_returns_causes_of_all_ClusterHealthChecks_whichever_the HealthCheckerImpl underTest = new HealthCheckerImpl(webServer, new NodeHealthCheck[0], clusterHealthChecks, sharedHealthState); - assertThat(underTest.checkCluster().getCauses()).containsOnly(expectedCauses); + assertThat(underTest.checkCluster().getHealth().getCauses()).containsOnly(expectedCauses); } @Test @@ -229,6 +229,18 @@ public void checkCluster_passes_set_of_NodeHealth_returns_by_HealthState_to_all_ } } + @Test + public void checkCluster_returns_NodeHealths_returned_by_HealthState() { + when(webServer.isStandalone()).thenReturn(false); + Set nodeHealths = IntStream.range(0, 1 + random.nextInt(4)).mapToObj(i -> randomNodeHealth()).collect(Collectors.toSet()); + when(sharedHealthState.readAll()).thenReturn(nodeHealths); + + HealthCheckerImpl underTest = new HealthCheckerImpl(webServer, new NodeHealthCheck[0], new ClusterHealthCheck[0], sharedHealthState); + + ClusterHealth clusterHealth = underTest.checkCluster(); + assertThat(clusterHealth.getNodes()).isEqualTo(nodeHealths); + } + private NodeHealth randomNodeHealth() { return newNodeHealthBuilder() .setStatus(NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)]) diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionModuleTest.java index 2ee0421796bb..e3090b0afefd 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionModuleTest.java @@ -47,7 +47,9 @@ public void verify_action_and_HealthChecker() { assertThat(classesAddedToContainer(container)) .contains(HealthCheckerImpl.class) - .contains(HealthAction.class); + .contains(HealthActionSupport.class) + .contains(HealthAction.class) + .doesNotContain(SafeModeHealthAction.class); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionTest.java index 63c94dff67e5..e98f5433520b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionTest.java @@ -19,26 +19,49 @@ */ package org.sonar.server.platform.ws; +import com.google.common.collect.ImmutableSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; import java.util.Random; import java.util.stream.IntStream; import org.apache.commons.lang.RandomStringUtils; import org.junit.Test; import org.sonar.api.server.ws.WebService; +import org.sonar.cluster.health.NodeDetails; +import org.sonar.cluster.health.NodeHealth; +import org.sonar.server.health.ClusterHealth; import org.sonar.server.health.Health; import org.sonar.server.health.HealthChecker; +import org.sonar.server.platform.WebServer; import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.TestResponse; import org.sonar.server.ws.WsActionTester; -import org.sonar.test.JsonAssert; import org.sonarqube.ws.WsSystem; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.api.utils.DateUtils.parseDateTime; +import static org.sonar.cluster.health.NodeDetails.newNodeDetailsBuilder; +import static org.sonar.cluster.health.NodeHealth.newNodeHealthBuilder; +import static org.sonar.server.health.Health.GREEN; import static org.sonar.server.health.Health.newHealthCheckBuilder; +import static org.sonar.test.JsonAssert.assertJson; public class HealthActionTest { - private HealthChecker mockedHealthChecker = mock(HealthChecker.class); - private WsActionTester underTest = new WsActionTester(new HealthAction(mockedHealthChecker)); + private final Random random = new Random(); + private HealthChecker healthChecker = mock(HealthChecker.class); + private WebServer webServer = mock(WebServer.class); + private WsActionTester underTest = new WsActionTester(new HealthAction(webServer, new HealthActionSupport(healthChecker))); @Test public void verify_definition() { @@ -54,26 +77,94 @@ public void verify_definition() { } @Test - public void verify_example() { - when(mockedHealthChecker.checkNode()).thenReturn( - newHealthCheckBuilder() - .setStatus(Health.Status.YELLOW) - .addCause("Elasticsearch status is YELLOW") - .build()); - TestRequest request = underTest.newRequest(); + public void verify_response_example() { + when(webServer.isStandalone()).thenReturn(false); + long time = parseDateTime("2015-08-13T23:34:59+0200").getTime(); + when(healthChecker.checkCluster()) + .thenReturn( + new ClusterHealth(newHealthCheckBuilder() + .setStatus(Health.Status.RED) + .addCause("Application node app-1 is RED") + .build(), + ImmutableSet.of( + newNodeHealthBuilder() + .setStatus(NodeHealth.Status.RED) + .addCause("foo") + .setDetails( + newNodeDetailsBuilder() + .setName("app-1") + .setType(NodeDetails.Type.APPLICATION) + .setHost("192.168.1.1") + .setPort(999) + .setStarted(time) + .build()) + .setDate(1 + random.nextInt(888)) + .build(), + newNodeHealthBuilder() + .setStatus(NodeHealth.Status.YELLOW) + .addCause("bar") + .setDetails( + newNodeDetailsBuilder() + .setName("app-2") + .setType(NodeDetails.Type.APPLICATION) + .setHost("192.168.1.2") + .setPort(999) + .setStarted(time) + .build()) + .setDate(1 + random.nextInt(888)) + .build(), + newNodeHealthBuilder() + .setStatus(NodeHealth.Status.GREEN) + .setDetails( + newNodeDetailsBuilder() + .setName("es-1") + .setType(NodeDetails.Type.SEARCH) + .setHost("192.168.1.3") + .setPort(999) + .setStarted(time) + .build()) + .setDate(1 + random.nextInt(888)) + .build(), + newNodeHealthBuilder() + .setStatus(NodeHealth.Status.GREEN) + .setDetails( + newNodeDetailsBuilder() + .setName("es-2") + .setType(NodeDetails.Type.SEARCH) + .setHost("192.168.1.4") + .setPort(999) + .setStarted(time) + .build()) + .setDate(1 + random.nextInt(888)) + .build(), + newNodeHealthBuilder() + .setStatus(NodeHealth.Status.GREEN) + .setDetails( + newNodeDetailsBuilder() + .setName("es-3") + .setType(NodeDetails.Type.SEARCH) + .setHost("192.168.1.5") + .setPort(999) + .setStarted(time) + .build()) + .setDate(1 + random.nextInt(888)) + .build()))); + + TestResponse response = underTest.newRequest().execute(); - JsonAssert.assertJson(request.execute().getInput()) - .isSimilarTo(underTest.getDef().responseExampleAsString()); + assertJson(response.getInput()) + .isSimilarTo(underTest.getDef().responseExampleAsString()); } @Test - public void request_returns_status_and_causes_from_HealthChecker_checkNode_method() { + public void request_returns_status_and_causes_from_HealthChecker_checkNode_method_when_standalone() { Health.Status randomStatus = Health.Status.values()[new Random().nextInt(Health.Status.values().length)]; Health.Builder builder = newHealthCheckBuilder() - .setStatus(randomStatus); + .setStatus(randomStatus); IntStream.range(0, new Random().nextInt(5)).mapToObj(i -> RandomStringUtils.randomAlphanumeric(3)).forEach(builder::addCause); Health health = builder.build(); - when(mockedHealthChecker.checkNode()).thenReturn(health); + when(healthChecker.checkNode()).thenReturn(health); + when(webServer.isStandalone()).thenReturn(true); TestRequest request = underTest.newRequest(); WsSystem.HealthResponse healthResponse = request.executeProtobuf(WsSystem.HealthResponse.class); @@ -81,4 +172,107 @@ public void request_returns_status_and_causes_from_HealthChecker_checkNode_metho assertThat(health.getCauses()).isEqualTo(health.getCauses()); } + @Test + public void response_contains_status_and_causes_from_HealthChecker_checkCluster_when_standalone() { + Health.Status randomStatus = Health.Status.values()[random.nextInt(Health.Status.values().length)]; + String[] causes = IntStream.range(0, random.nextInt(33)).mapToObj(i -> randomAlphanumeric(4)).toArray(String[]::new); + Health.Builder healthBuilder = newHealthCheckBuilder() + .setStatus(randomStatus); + Arrays.stream(causes).forEach(healthBuilder::addCause); + when(webServer.isStandalone()).thenReturn(false); + when(healthChecker.checkCluster()).thenReturn(new ClusterHealth(healthBuilder.build(), emptySet())); + + WsSystem.HealthResponse clusterHealthResponse = underTest.newRequest().executeProtobuf(WsSystem.HealthResponse.class); + assertThat(clusterHealthResponse.getHealth().name()).isEqualTo(randomStatus.name()); + assertThat(clusterHealthResponse.getCausesList()) + .extracting(WsSystem.Cause::getMessage) + .containsOnly(causes); + } + + @Test + public void response_contains_information_of_nodes_when_clustered() { + NodeHealth nodeHealth = randomNodeHealth(); + when(webServer.isStandalone()).thenReturn(false); + when(healthChecker.checkCluster()).thenReturn(new ClusterHealth(GREEN, singleton(nodeHealth))); + + WsSystem.HealthResponse response = underTest.newRequest().executeProtobuf(WsSystem.HealthResponse.class); + + assertThat(response.getNodes().getNodesList()) + .hasSize(1); + WsSystem.Node node = response.getNodes().getNodesList().iterator().next(); + assertThat(node.getHealth().name()).isEqualTo(nodeHealth.getStatus().name()); + assertThat(node.getCausesList()) + .extracting(WsSystem.Cause::getMessage) + .containsOnly(nodeHealth.getCauses().stream().toArray(String[]::new)); + assertThat(node.getName()).isEqualTo(nodeHealth.getDetails().getName()); + assertThat(node.getHost()).isEqualTo(nodeHealth.getDetails().getHost()); + assertThat(node.getPort()).isEqualTo(String.valueOf(nodeHealth.getDetails().getPort())); + assertThat(node.getStarted()).isEqualTo(formatDateTime(nodeHealth.getDetails().getStarted())); + assertThat(node.getType().name()).isEqualTo(nodeHealth.getDetails().getType().name()); + } + + @Test + public void response_sort_nodes_by_type_name_host_then_port_when_clustered() { + // using created field as a unique identifier. pseudo random value to ensure sorting is not based on created field + List nodeHealths = new ArrayList<>(Arrays.asList( + randomNodeHealth(NodeDetails.Type.APPLICATION, "1_name", "1_host", 1, 99), + randomNodeHealth(NodeDetails.Type.APPLICATION, "1_name", "2_host", 1, 85), + randomNodeHealth(NodeDetails.Type.APPLICATION, "1_name", "2_host", 2, 12), + randomNodeHealth(NodeDetails.Type.APPLICATION, "2_name", "1_host", 1, 6), + randomNodeHealth(NodeDetails.Type.APPLICATION, "2_name", "1_host", 2, 30), + randomNodeHealth(NodeDetails.Type.APPLICATION, "2_name", "2_host", 1, 75), + randomNodeHealth(NodeDetails.Type.APPLICATION, "2_name", "2_host", 2, 258), + randomNodeHealth(NodeDetails.Type.SEARCH, "1_name", "1_host", 1, 963), + randomNodeHealth(NodeDetails.Type.SEARCH, "1_name", "1_host", 2, 1), + randomNodeHealth(NodeDetails.Type.SEARCH, "1_name", "2_host", 1, 35), + randomNodeHealth(NodeDetails.Type.SEARCH, "1_name", "2_host", 2, 45), + randomNodeHealth(NodeDetails.Type.SEARCH, "2_name", "1_host", 1, 39), + randomNodeHealth(NodeDetails.Type.SEARCH, "2_name", "1_host", 2, 28), + randomNodeHealth(NodeDetails.Type.SEARCH, "2_name", "2_host", 1, 66), + randomNodeHealth(NodeDetails.Type.SEARCH, "2_name", "2_host", 2, 77))); + String[] expected = nodeHealths.stream().map(s -> formatDateTime(new Date(s.getDetails().getStarted()))).toArray(String[]::new); + Collections.shuffle(nodeHealths); + + when(webServer.isStandalone()).thenReturn(false); + when(healthChecker.checkCluster()).thenReturn(new ClusterHealth(GREEN, new HashSet<>(nodeHealths))); + + WsSystem.HealthResponse response = underTest.newRequest().executeProtobuf(WsSystem.HealthResponse.class); + + assertThat(response.getNodes().getNodesList()) + .extracting(WsSystem.Node::getStarted) + .containsExactly(expected); + } + + private NodeHealth randomNodeHealth() { + NodeHealth.Builder builder = newNodeHealthBuilder() + .setStatus(NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)]); + IntStream.range(0, random.nextInt(4)).mapToObj(i -> randomAlphabetic(5)).forEach(builder::addCause); + return builder.setDetails( + NodeDetails.newNodeDetailsBuilder() + .setType(random.nextBoolean() ? NodeDetails.Type.APPLICATION : NodeDetails.Type.SEARCH) + .setName(randomAlphanumeric(3)) + .setHost(randomAlphanumeric(4)) + .setPort(1 + random.nextInt(3)) + .setStarted(1 + random.nextInt(23)) + .build()) + .setDate(1 + random.nextInt(343)) + .build(); + } + + private NodeHealth randomNodeHealth(NodeDetails.Type type, String name, String host, int port, long started) { + NodeHealth.Builder builder = newNodeHealthBuilder() + .setStatus(NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)]); + IntStream.range(0, random.nextInt(4)).mapToObj(i -> randomAlphabetic(5)).forEach(builder::addCause); + return builder.setDetails( + NodeDetails.newNodeDetailsBuilder() + .setType(type) + .setName(name) + .setHost(host) + .setPort(port) + .setStarted(started) + .build()) + .setDate(1 + random.nextInt(23)) + .build(); + } + } diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionModuleTest.java index 896e35ed0a7a..2c7edb56941a 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionModuleTest.java @@ -44,7 +44,9 @@ public void verify_action_and_HealthChecker() { assertThat(classesAddedToContainer(container)) .contains(HealthCheckerImpl.class) - .contains(HealthAction.class); + .contains(HealthActionSupport.class) + .contains(SafeModeHealthAction.class) + .doesNotContain(HealthAction.class); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionTest.java new file mode 100644 index 000000000000..340171ce67c7 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionTest.java @@ -0,0 +1,106 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.ws; + +import java.util.Arrays; +import java.util.Random; +import java.util.stream.IntStream; +import org.apache.commons.lang.RandomStringUtils; +import org.junit.Test; +import org.sonar.api.server.ws.WebService; +import org.sonar.server.health.Health; +import org.sonar.server.health.HealthChecker; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.TestResponse; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.WsSystem; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.server.health.Health.newHealthCheckBuilder; +import static org.sonar.test.JsonAssert.assertJson; + +public class SafeModeHealthActionTest { + private final Random random = new Random(); + private HealthChecker healthChecker = mock(HealthChecker.class); + private WsActionTester underTest = new WsActionTester(new SafeModeHealthAction(new HealthActionSupport(healthChecker))); + + @Test + public void verify_definition() { + WebService.Action definition = underTest.getDef(); + + assertThat(definition.key()).isEqualTo("health"); + assertThat(definition.isPost()).isFalse(); + assertThat(definition.description()).isNotEmpty(); + assertThat(definition.since()).isEqualTo("6.6"); + assertThat(definition.isInternal()).isFalse(); + assertThat(definition.responseExample()).isNotNull(); + assertThat(definition.params()).isEmpty(); + } + + @Test + public void verify_response_example() { + when(healthChecker.checkNode()) + .thenReturn(newHealthCheckBuilder() + .setStatus(Health.Status.RED) + .addCause("Application node app-1 is RED") + .build()); + + TestResponse response = underTest.newRequest().execute(); + + assertJson(response.getInput()) + .ignoreFields("nodes") + .isSimilarTo(underTest.getDef().responseExampleAsString()); + } + + @Test + public void request_returns_status_and_causes_from_HealthChecker_checkNode_method() { + Health.Status randomStatus = Health.Status.values()[new Random().nextInt(Health.Status.values().length)]; + Health.Builder builder = newHealthCheckBuilder() + .setStatus(randomStatus); + IntStream.range(0, new Random().nextInt(5)).mapToObj(i -> RandomStringUtils.randomAlphanumeric(3)).forEach(builder::addCause); + Health health = builder.build(); + when(healthChecker.checkNode()).thenReturn(health); + TestRequest request = underTest.newRequest(); + + WsSystem.HealthResponse healthResponse = request.executeProtobuf(WsSystem.HealthResponse.class); + assertThat(healthResponse.getHealth().name()).isEqualTo(randomStatus.name()); + assertThat(health.getCauses()).isEqualTo(health.getCauses()); + } + + @Test + public void response_contains_status_and_causes_from_HealthChecker_checkCluster() { + Health.Status randomStatus = Health.Status.values()[random.nextInt(Health.Status.values().length)]; + String[] causes = IntStream.range(0, random.nextInt(33)).mapToObj(i -> randomAlphanumeric(4)).toArray(String[]::new); + Health.Builder healthBuilder = newHealthCheckBuilder() + .setStatus(randomStatus); + Arrays.stream(causes).forEach(healthBuilder::addCause); + when(healthChecker.checkNode()).thenReturn(healthBuilder.build()); + + WsSystem.HealthResponse clusterHealthResponse = underTest.newRequest().executeProtobuf(WsSystem.HealthResponse.class); + assertThat(clusterHealthResponse.getHealth().name()).isEqualTo(randomStatus.name()); + assertThat(clusterHealthResponse.getCausesList()) + .extracting(WsSystem.Cause::getMessage) + .containsOnly(causes); + } + +} diff --git a/sonar-ws/src/main/protobuf/ws-system.proto b/sonar-ws/src/main/protobuf/ws-system.proto index 39ad9210eb7b..874d838ffc9f 100644 --- a/sonar-ws/src/main/protobuf/ws-system.proto +++ b/sonar-ws/src/main/protobuf/ws-system.proto @@ -28,6 +28,11 @@ option optimize_for = SPEED; message HealthResponse { optional Health health = 1; repeated Cause causes = 2; + optional Nodes nodes = 3; +} + +message Nodes { + repeated Node nodes = 1; } // GET api/system/status @@ -55,3 +60,18 @@ enum Status { DB_MIGRATION_NEEDED = 4; DB_MIGRATION_RUNNING = 5; } + +message Node { + optional string name = 1; + optional NodeType type = 2; + optional string host = 3; + optional string port = 4; + optional string started = 5; + optional Health health = 6; + repeated Cause causes = 7; +} + +enum NodeType { + APPLICATION = 0; + SEARCH = 1; +}