SONAR-9741 return nodes in response of api/system/health

This commit is contained in:
Sébastien Lesaint 2017-09-04 15:54:38 +02:00
parent fa0fe58b05
commit 9250ab169f
17 changed files with 786 additions and 55 deletions

View File

@ -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<NodeHealth> nodes;
public ClusterHealth(Health health, Set<NodeHealth> 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<NodeHealth> 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 +
'}';
}
}

View File

@ -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();

View File

@ -31,5 +31,5 @@ public interface HealthChecker {
*
* @throws IllegalStateException if clustering is not enabled.
*/
Health checkCluster();
ClusterHealth checkCluster();
}

View File

@ -70,14 +70,15 @@ public class HealthCheckerImpl implements HealthChecker {
}
@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<NodeHealth> 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<Health> {

View File

@ -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." +
"<p>status: the health status" +
" <ul>" +
" <li>GREEN: SonarQube is fully operational</li>" +
" <li>YELLOW: SonarQube is operational but something must be fixed to be fully operational</li>" +
" <li>RED: SonarQube is not operational</li>" +
" </ul>" +
"</p>")
.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);
}
}
}

View File

@ -41,6 +41,7 @@ public class HealthActionModule extends Module {
add(EsStatusClusterCheck.class);
add(HealthCheckerImpl.class,
HealthActionSupport.class,
HealthAction.class);
}
}

View File

@ -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<NodeHealth> NODE_HEALTH_COMPARATOR = Comparator.<NodeHealth>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." +
"<p>Require 'Administer System' permission or authentication with passcode</p>" +
"<p> " +
" <ul>" +
" <li>GREEN: SonarQube is fully operational</li>" +
" <li>YELLOW: SonarQube is usable, but it needs attention in order to be fully operational</li>" +
" <li>RED: SonarQube is not operational</li>" +
" </ul>" +
"</p>")
.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();
}
}

View File

@ -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);
}
}

View File

@ -35,6 +35,7 @@ public class SafeModeHealthActionModule extends Module {
EsStatusNodeCheck.class,
HealthCheckerImpl.class,
HealthAction.class);
HealthActionSupport.class,
SafeModeHealthAction.class);
}
}

View File

@ -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": []
}
]
}

View File

@ -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<NodeHealth> 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<NodeHealth> 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<NodeHealth> 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<NodeHealth> 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<NodeHealth> 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());
}
}

View File

@ -145,7 +145,7 @@ public class HealthCheckerImplTest {
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 class HealthCheckerImplTest {
List<Health.Status> 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 class HealthCheckerImplTest {
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 class HealthCheckerImplTest {
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 class HealthCheckerImplTest {
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 class HealthCheckerImplTest {
}
}
@Test
public void checkCluster_returns_NodeHealths_returned_by_HealthState() {
when(webServer.isStandalone()).thenReturn(false);
Set<NodeHealth> 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)])

View File

@ -47,7 +47,9 @@ public class HealthActionModuleTest {
assertThat(classesAddedToContainer(container))
.contains(HealthCheckerImpl.class)
.contains(HealthAction.class);
.contains(HealthActionSupport.class)
.contains(HealthAction.class)
.doesNotContain(SafeModeHealthAction.class);
}
@Test

View File

@ -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 class HealthActionTest {
}
@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())));
JsonAssert.assertJson(request.execute().getInput())
.isSimilarTo(underTest.getDef().responseExampleAsString());
TestResponse response = underTest.newRequest().execute();
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 class HealthActionTest {
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<NodeHealth> 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();
}
}

View File

@ -44,7 +44,9 @@ public class SafeModeHealthActionModuleTest {
assertThat(classesAddedToContainer(container))
.contains(HealthCheckerImpl.class)
.contains(HealthAction.class);
.contains(HealthActionSupport.class)
.contains(SafeModeHealthAction.class)
.doesNotContain(HealthAction.class);
}
@Test

View File

@ -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);
}
}

View File

@ -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;
}