diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-08-23 16:26:39 +0200 |
---|---|---|
committer | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-09-13 15:50:47 +0200 |
commit | eb6fa5835db8dc3fdc55d75bb13c70464a3e7305 (patch) | |
tree | a62c10fcca001a40af41c70650f9e38ff1536630 | |
parent | da817d1a7f8511c1ee6e7a233df16c577dd961d7 (diff) | |
download | sonarqube-eb6fa5835db8dc3fdc55d75bb13c70464a3e7305.tar.gz sonarqube-eb6fa5835db8dc3fdc55d75bb13c70464a3e7305.zip |
SONAR-9739 add WS api/system/health
29 files changed, 1700 insertions, 1 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/DbConnectionCheck.java b/server/sonar-server/src/main/java/org/sonar/server/health/DbConnectionCheck.java new file mode 100644 index 00000000000..7d43d29f0ae --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/DbConnectionCheck.java @@ -0,0 +1,59 @@ +/* + * 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 org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.IsAliveMapper; + +import static org.sonar.server.health.Health.newHealthCheckBuilder; + +/** + * Checks Web Server can connect to the Database. + */ +public class DbConnectionCheck implements HealthCheck { + private static final Logger LOGGER = Loggers.get(DbConnectionCheck.class); + private static final Health RED_HEALTH = newHealthCheckBuilder().setStatus(Health.Status.RED).addCause("Can't connect to DB").build(); + + private final DbClient dbClient; + + public DbConnectionCheck(DbClient dbClient) { + this.dbClient = dbClient; + } + + @Override + public Health check() { + if (isConnectedToDB()) { + return Health.GREEN; + } + return RED_HEALTH; + } + + private boolean isConnectedToDB() { + try (DbSession dbSession = dbClient.openSession(false)) { + return dbSession.getMapper(IsAliveMapper.class).isAlive() == IsAliveMapper.IS_ALIVE_RETURNED_VALUE; + } catch (RuntimeException e) { + LOGGER.trace("DB connection is down", e); + return false; + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusCheck.java b/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusCheck.java new file mode 100644 index 00000000000..cdf086082c9 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusCheck.java @@ -0,0 +1,60 @@ +/* + * 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 org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.sonar.server.es.EsClient; + +import static org.sonar.server.health.Health.newHealthCheckBuilder; + +/** + * Checks the ElasticSearch cluster status. + */ +public class EsStatusCheck implements HealthCheck { + private static final Health YELLOW_HEALTH = newHealthCheckBuilder() + .setStatus(Health.Status.YELLOW) + .addCause("Elasticsearch status is YELLOW") + .build(); + private static final Health RED_HEALTH = newHealthCheckBuilder() + .setStatus(Health.Status.RED) + .addCause("Elasticsearch status is RED") + .build(); + + private final EsClient esClient; + + public EsStatusCheck(EsClient esClient) { + this.esClient = esClient; + } + + @Override + public Health check() { + ClusterHealthStatus esStatus = esClient.prepareClusterStats().get().getStatus(); + switch (esStatus) { + case GREEN: + return Health.GREEN; + case YELLOW: + return YELLOW_HEALTH; + case RED: + return RED_HEALTH; + default: + throw new IllegalArgumentException("Unsupported Elasticsearch status " + esStatus); + } + } +} 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 new file mode 100644 index 00000000000..065b64a5771 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/Health.java @@ -0,0 +1,136 @@ +/* + * 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 com.google.common.collect.ImmutableSet; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +public class Health { + /** + * The GREEN status without any cause as a constant, for convenience and optimisation. + */ + static final Health GREEN = newHealthCheckBuilder() + .setStatus(Status.GREEN) + .build(); + + private final Status status; + private final Set<String> causes; + + public Health(Builder builder) { + this.status = builder.status; + this.causes = ImmutableSet.copyOf(builder.causes); + } + + public Status getStatus() { + return status; + } + + public Set<String> getCauses() { + return causes; + } + + public static Builder newHealthCheckBuilder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Health health = (Health) o; + return status == health.status && + Objects.equals(causes, health.causes); + } + + @Override + public int hashCode() { + return Objects.hash(status, causes); + } + + @Override + public String toString() { + return "Health{" + status + + ", causes=" + causes + + '}'; + } + + /** + * Builder of {@link Health} which supports being reused for optimization. + */ + public static class Builder { + private Status status; + private Set<String> causes = new HashSet<>(0); + + private Builder() { + // use static factory method + } + + public Builder clear() { + this.status = null; + this.causes.clear(); + return this; + } + + public Builder setStatus(Status status) { + this.status = checkStatus(status); + return this; + } + + public Builder addCause(String cause) { + requireNonNull(cause, "cause can't be null"); + checkArgument(!cause.trim().isEmpty(), "cause can't be empty"); + causes.add(cause); + return this; + } + + public Health build() { + checkStatus(this.status); + return new Health(this); + } + + private static Status checkStatus(Status status) { + return requireNonNull(status, "status can't be null"); + } + } + + public enum Status { + /** + * Fully working + */ + GREEN, + /** + * Yellow: Working but something must be fixed to make SQ fully operational + */ + YELLOW, + /** + * Red: Not working + */ + RED + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/HealthCheck.java b/server/sonar-server/src/main/java/org/sonar/server/health/HealthCheck.java new file mode 100644 index 00000000000..f52f9225003 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/HealthCheck.java @@ -0,0 +1,24 @@ +/* + * 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; + +public interface HealthCheck { + Health check(); +} 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 new file mode 100644 index 00000000000..48d1f94a273 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/HealthChecker.java @@ -0,0 +1,27 @@ +/* + * 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; + +public interface HealthChecker { + /** + * Perform a check of the health of SonarQube, either as a standalone node or as a cluster. + */ + Health check(); +} 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 new file mode 100644 index 00000000000..e45e8cd599c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/HealthCheckerImpl.java @@ -0,0 +1,77 @@ +/* + * 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.Arrays; +import java.util.List; +import java.util.function.BinaryOperator; +import java.util.stream.Stream; + +import static org.sonar.server.health.Health.newHealthCheckBuilder; + +/** + * Implementation of {@link HealthChecker} that executes implementations of {@link HealthCheck} in the container + * and aggregates their results. + */ +public class HealthCheckerImpl implements HealthChecker { + private final List<HealthCheck> healthChecks; + + public HealthCheckerImpl(HealthCheck... healthChecks) { + this.healthChecks = Arrays.asList(healthChecks); + } + + @Override + public Health check() { + return healthChecks.stream().map(HealthCheck::check) + .reduce(Health.GREEN, HealthReducer.INSTANCE); + } + + private enum HealthReducer implements BinaryOperator<Health> { + INSTANCE; + + /** + * According to Javadoc, {@link BinaryOperator} used in method + * {@link java.util.stream.Stream#reduce(Object, BinaryOperator)} is supposed to be stateless. + * + * But as we are sure this {@link BinaryOperator} won't be used on a Stream with {@link Stream#parallel()} + * feature on, we allow ourselves this optimisation. + */ + private final Health.Builder builder = newHealthCheckBuilder(); + + @Override + public Health apply(Health left, Health right) { + builder.clear(); + builder.setStatus(worseOf(left.getStatus(), right.getStatus())); + left.getCauses().forEach(builder::addCause); + right.getCauses().forEach(builder::addCause); + return builder.build(); + } + + private static Health.Status worseOf(Health.Status left, Health.Status right) { + if (left == right) { + return left; + } + if (left.ordinal() > right.ordinal()) { + return left; + } + return right; + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/WebServerSafemodeCheck.java b/server/sonar-server/src/main/java/org/sonar/server/health/WebServerSafemodeCheck.java new file mode 100644 index 00000000000..09a1cdc2e46 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/WebServerSafemodeCheck.java @@ -0,0 +1,39 @@ +/* + * 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 static org.sonar.server.health.Health.newHealthCheckBuilder; + +/** + * Checks the running status of the WebServer when the WebServer is in safemode. + * Obviously, it statically returns a red health status. + */ +public class WebServerSafemodeCheck implements HealthCheck { + + private static final Health RED_HEALTH = newHealthCheckBuilder() + .setStatus(Health.Status.RED) + .addCause("SonarQube webserver is not up") + .build(); + + @Override + public Health check() { + return RED_HEALTH; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/WebServerStatusCheck.java b/server/sonar-server/src/main/java/org/sonar/server/health/WebServerStatusCheck.java new file mode 100644 index 00000000000..2146a315d4d --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/WebServerStatusCheck.java @@ -0,0 +1,59 @@ +/* + * 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.EnumSet; +import org.sonar.server.app.RestartFlagHolder; +import org.sonar.server.platform.Platform; +import org.sonar.server.platform.db.migration.DatabaseMigrationState; + +import static org.sonar.server.health.Health.newHealthCheckBuilder; + +/** + * Checks the running status of the WebServer when it is not anymore in safemode. + */ +public class WebServerStatusCheck implements HealthCheck { + private static final EnumSet<DatabaseMigrationState.Status> VALID_DATABASEMIGRATION_STATUSES = EnumSet.of( + DatabaseMigrationState.Status.NONE, DatabaseMigrationState.Status.SUCCEEDED); + + private final DatabaseMigrationState migrationState; + private final Platform platform; + private final RestartFlagHolder restartFlagHolder; + + public WebServerStatusCheck(DatabaseMigrationState migrationState, Platform platform, RestartFlagHolder restartFlagHolder) { + this.migrationState = migrationState; + this.platform = platform; + this.restartFlagHolder = restartFlagHolder; + } + + @Override + public Health check() { + Platform.Status platformStatus = platform.status(); + if (platformStatus == Platform.Status.UP + && VALID_DATABASEMIGRATION_STATUSES.contains(migrationState.getStatus()) + && !restartFlagHolder.isRestarting()) { + return Health.GREEN; + } + return newHealthCheckBuilder() + .setStatus(Health.Status.RED) + .addCause("SonarQube webserver is not up") + .build(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/health/package-info.java new file mode 100644 index 00000000000..ce49c888b43 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.health; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 6786bec8638..bda21231f0a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -117,6 +117,7 @@ import org.sonar.server.platform.web.WebPagesFilter; import org.sonar.server.platform.web.requestid.HttpRequestIdModule; import org.sonar.server.platform.ws.ChangeLogLevelAction; import org.sonar.server.platform.ws.DbMigrationStatusAction; +import org.sonar.server.platform.ws.HealthActionModule; import org.sonar.server.platform.ws.InfoActionModule; import org.sonar.server.platform.ws.L10nWs; import org.sonar.server.platform.ws.LogsAction; @@ -481,7 +482,6 @@ public class PlatformLevel4 extends PlatformLevel { PingAction.class, UpgradesAction.class, StatusAction.class, - SystemWs.class, SystemMonitor.class, SettingsMonitor.class, SonarQubeMonitor.class, @@ -493,6 +493,8 @@ public class PlatformLevel4 extends PlatformLevel { LogsAction.class, ChangeLogLevelAction.class, DbMigrationStatusAction.class, + HealthActionModule.class, + SystemWs.class, // Server id ServerIdWsModule.class, diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java index 23208b88d33..95ba0f135e2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java @@ -19,6 +19,7 @@ */ package org.sonar.server.platform.platformlevel; +import org.sonar.server.platform.ws.SafeModeHealthActionModule; import org.sonar.server.organization.NoopDefaultOrganizationCache; import org.sonar.server.platform.ServerImpl; import org.sonar.server.platform.db.migration.AutoDbMigration; @@ -55,6 +56,7 @@ public class PlatformLevelSafeMode extends PlatformLevel { StatusAction.class, MigrateDbAction.class, DbMigrationStatusAction.class, + SafeModeHealthActionModule.class, SystemWs.class, // Listing WS diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/AbstractHealthAction.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/AbstractHealthAction.java new file mode 100644 index 00000000000..c0a878b8b5e --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/AbstractHealthAction.java @@ -0,0 +1,68 @@ +/* + * 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 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.ws.WsUtils; +import org.sonarqube.ws.WsSystem; + +public abstract class AbstractHealthAction implements SystemWsAction { + private final HealthChecker healthChecker; + + public AbstractHealthAction(HealthChecker healthChecker) { + this.healthChecker = healthChecker; + } + + @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); + } + + @Override + public void handle(Request request, Response response) throws Exception { + performAuthenticationChecks(); + + Health check = healthChecker.check(); + 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); + } + + abstract void performAuthenticationChecks(); +} 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 new file mode 100644 index 00000000000..ab160d65e57 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthAction.java @@ -0,0 +1,39 @@ +/* + * 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.server.health.HealthChecker; +import org.sonar.server.user.UserSession; + +public class HealthAction extends AbstractHealthAction { + private final UserSession userSession; + + public HealthAction(UserSession userSession, HealthChecker healthChecker) { + super(healthChecker); + this.userSession = userSession; + } + + @Override + void performAuthenticationChecks() { + userSession + .checkLoggedIn() + .checkIsSystemAdministrator(); + } +} 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 new file mode 100644 index 00000000000..3e9b97aa487 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java @@ -0,0 +1,41 @@ +/* + * 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.core.platform.Module; +import org.sonar.server.health.DbConnectionCheck; +import org.sonar.server.health.EsStatusCheck; +import org.sonar.server.health.HealthCheckerImpl; +import org.sonar.server.health.WebServerStatusCheck; + +public class HealthActionModule extends Module { + @Override + protected void configureModule() { + add( + // HealthCheck implementations + WebServerStatusCheck.class, + DbConnectionCheck.class, + EsStatusCheck.class, + + HealthCheckerImpl.class, + + HealthAction.class); + } +} 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 00000000000..f3248871603 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthAction.java @@ -0,0 +1,33 @@ +/* + * 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.server.health.HealthChecker; + +public class SafeModeHealthAction extends AbstractHealthAction { + public SafeModeHealthAction(HealthChecker healthChecker) { + super(healthChecker); + } + + @Override + void performAuthenticationChecks() { + // no authentication check in safemode + } +} 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 new file mode 100644 index 00000000000..a11df47a697 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthActionModule.java @@ -0,0 +1,40 @@ +/* + * 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.core.platform.Module; +import org.sonar.server.health.DbConnectionCheck; +import org.sonar.server.health.EsStatusCheck; +import org.sonar.server.health.HealthCheckerImpl; +import org.sonar.server.health.WebServerSafemodeCheck; + +public class SafeModeHealthActionModule extends Module { + @Override + protected void configureModule() { + add( + // HealthCheck implementations + WebServerSafemodeCheck.class, + DbConnectionCheck.class, + EsStatusCheck.class, + + HealthCheckerImpl.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 new file mode 100644 index 00000000000..9d95994a7b4 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/platform/ws/example-health.json @@ -0,0 +1,8 @@ +{ + "health": "YELLOW", + "causes": [ + { + "message": "Elasticsearch status is YELLOW" + } + ] +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/health/DbConnectionCheckTest.java b/server/sonar-server/src/test/java/org/sonar/server/health/DbConnectionCheckTest.java new file mode 100644 index 00000000000..41b8c3acd51 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/health/DbConnectionCheckTest.java @@ -0,0 +1,81 @@ +/* + * 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 org.junit.Before; +import org.junit.Test; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.IsAliveMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DbConnectionCheckTest { + private DbClient dbClient = mock(DbClient.class); + private DbSession dbSession = mock(DbSession.class); + private IsAliveMapper isAliveMapper = mock(IsAliveMapper.class); + + private DbConnectionCheck underTest = new DbConnectionCheck(dbClient); + + @Before + public void wireMocks() { + when(dbClient.openSession(anyBoolean())).thenReturn(dbSession); + when(dbSession.getMapper(IsAliveMapper.class)).thenReturn(isAliveMapper); + } + + @Test + public void status_is_GREEN_without_cause_if_isAlive_returns_1() { + when(isAliveMapper.isAlive()).thenReturn(1); + + Health health = underTest.check(); + + assertThat(health).isEqualTo(Health.GREEN); + } + + @Test + public void status_is_RED_with_single_cause_if_any_error_occurs_when_checking_DB() throws Exception { + when(isAliveMapper.isAlive()).thenThrow(new RuntimeException("simulated runtime exception when querying DB")); + + Health health = underTest.check(); + + verifyRedStatus(health); + } + + /** + * By contract {@link IsAliveMapper#isAlive()} can not return anything but 1. Still we write this test as a + * protection against change in this contract. + */ + @Test + public void status_is_RED_with_single_cause_if_isAlive_does_not_return_1() throws Exception { + when(isAliveMapper.isAlive()).thenReturn(12); + + Health health = underTest.check(); + + verifyRedStatus(health); + } + + private void verifyRedStatus(Health health) { + assertThat(health.getStatus()).isEqualTo(Health.Status.RED); + assertThat(health.getCauses()).containsOnly("Can't connect to DB"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/health/EsStatusCheckTest.java b/server/sonar-server/src/test/java/org/sonar/server/health/EsStatusCheckTest.java new file mode 100644 index 00000000000..0f8f0183843 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/health/EsStatusCheckTest.java @@ -0,0 +1,42 @@ +/* + * 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 org.junit.Rule; +import org.junit.Test; +import org.sonar.server.es.EsTester; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EsStatusCheckTest { + + @Rule + public EsTester esTester = new EsTester(); + + private EsStatusCheck underTest = new EsStatusCheck(esTester.client()); + + @Test + public void check_returns_GREEN_without_cause_if_ES_cluster_status_is_GREEN() { + Health health = underTest.check(); + + assertThat(health).isEqualTo(Health.GREEN); + } + +} 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 new file mode 100644 index 00000000000..ae91caeee7e --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/health/HealthCheckerImplTest.java @@ -0,0 +1,126 @@ +/* + * 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.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.lang.RandomStringUtils; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.server.health.Health.Status.GREEN; +import static org.sonar.server.health.Health.Status.RED; +import static org.sonar.server.health.Health.Status.YELLOW; + +public class HealthCheckerImplTest { + + private final Random random = new Random(); + + @Test + public void check_returns_green_status_without_any_cause_when_there_is_no_HealthCheck() { + HealthCheckerImpl underTest = new HealthCheckerImpl(); + + assertThat(underTest.check()).isEqualTo(Health.GREEN); + } + + @Test + public void checks_returns_GREEN_status_if_only_GREEN_statuses_returned_by_HealthChecks() { + List<Health.Status> statuses = IntStream.range(1, 1 + random.nextInt(20)).mapToObj(i -> GREEN).collect(Collectors.toList()); + HealthCheckerImpl underTest = newHealthCheckerImpl(statuses.stream()); + + assertThat(underTest.check().getStatus()) + .describedAs("%s should have been computed from %s statuses", GREEN, statuses) + .isEqualTo(GREEN); + } + + @Test + public void checks_returns_YELLOW_status_if_only_GREEN_and_at_least_one_YELLOW_statuses_returned_by_HealthChecks() { + List<Health.Status> statuses = new ArrayList<>(); + Stream.concat( + IntStream.range(0, 1 + random.nextInt(20)).mapToObj(i -> YELLOW), // at least 1 YELLOW + IntStream.range(0, random.nextInt(20)).mapToObj(i -> GREEN)).forEach(statuses::add); // between 0 and 19 GREEN + Collections.shuffle(statuses); + HealthCheckerImpl underTest = newHealthCheckerImpl(statuses.stream()); + + assertThat(underTest.check().getStatus()) + .describedAs("%s should have been computed from %s statuses", YELLOW, statuses) + .isEqualTo(YELLOW); + } + + @Test + public void checks_returns_RED_status_if_at_least_one_RED_status_returned_by_HealthChecks() { + List<Health.Status> statuses = new ArrayList<>(); + Stream.of( + IntStream.range(0, 1 + random.nextInt(20)).mapToObj(i -> RED), // at least 1 RED + IntStream.range(0, random.nextInt(20)).mapToObj(i -> YELLOW), // between 0 and 19 YELLOW + IntStream.range(0, random.nextInt(20)).mapToObj(i -> GREEN) // between 0 and 19 GREEN + ).flatMap(s -> s) + .forEach(statuses::add); + Collections.shuffle(statuses); + HealthCheckerImpl underTest = newHealthCheckerImpl(statuses.stream()); + + assertThat(underTest.check().getStatus()) + .describedAs("%s should have been computed from %s statuses", RED, statuses) + .isEqualTo(RED); + } + + @Test + public void checks_returns_causes_of_all_HealthChecks_whichever_their_status() { + HealthCheck[] healthChecks = IntStream.range(0, 1 + random.nextInt(20)) + .mapToObj(s -> new StaticHealthCheck(IntStream.range(0, random.nextInt(3)).mapToObj(i -> RandomStringUtils.randomAlphanumeric(3)).toArray(String[]::new))) + .map(HealthCheck.class::cast) + .toArray(HealthCheck[]::new); + String[] expected = Arrays.stream(healthChecks).map(HealthCheck::check).flatMap(s -> s.getCauses().stream()).toArray(String[]::new); + + HealthCheckerImpl underTest = new HealthCheckerImpl(healthChecks); + + assertThat(underTest.check().getCauses()).containsOnly(expected); + } + + private HealthCheckerImpl newHealthCheckerImpl(Stream<Health.Status> statuses) { + Stream<StaticHealthCheck> staticHealthCheckStream = statuses.map(StaticHealthCheck::new); + return new HealthCheckerImpl(staticHealthCheckStream.map(HealthCheck.class::cast).toArray(HealthCheck[]::new)); + } + + private class StaticHealthCheck implements HealthCheck { + private final Health health; + + public StaticHealthCheck(Health.Status status) { + this.health = Health.newHealthCheckBuilder().setStatus(status).build(); + } + + public StaticHealthCheck(String... causes) { + Health.Builder builder = Health.newHealthCheckBuilder().setStatus(Health.Status.values()[random.nextInt(3)]); + Stream.of(causes).forEach(builder::addCause); + this.health = builder.build(); + } + + @Override + public Health check() { + return health; + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/health/HealthTest.java b/server/sonar-server/src/test/java/org/sonar/server/health/HealthTest.java new file mode 100644 index 00000000000..00874a89d7a --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/health/HealthTest.java @@ -0,0 +1,184 @@ +/* + * 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 com.google.common.base.Strings; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.assertj.core.api.AbstractCharSequenceAssert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.server.health.Health.newHealthCheckBuilder; + +public class HealthTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private final Random random = new Random(); + private final Health.Status anyStatus = Health.Status.values()[random.nextInt(Health.Status.values().length)]; + private final Set<String> randomCauses = IntStream.range(0, random.nextInt(5)).mapToObj(s -> randomAlphanumeric(3)).collect(Collectors.toSet()); + + @Test + public void build_throws_NPE_if_status_is_null() { + Health.Builder builder = newHealthCheckBuilder(); + + expectStatusNotNullNPE(); + + builder.build(); + } + + @Test + public void setStatus_throws_NPE_if_status_is_null() { + Health.Builder builder = newHealthCheckBuilder(); + + expectStatusNotNullNPE(); + + builder.setStatus(null); + } + + @Test + public void getStatus_returns_status_from_builder() { + Health underTest = newHealthCheckBuilder().setStatus(anyStatus).build(); + + assertThat(underTest.getStatus()).isEqualTo(anyStatus); + } + + @Test + public void addCause_throws_NPE_if_arg_is_null() { + Health.Builder builder = newHealthCheckBuilder(); + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("cause can't be null"); + + builder.addCause(null); + } + + @Test + public void addCause_throws_IAE_if_arg_is_empty() { + Health.Builder builder = newHealthCheckBuilder(); + + expectCauseCannotBeEmptyIAE(); + + builder.addCause(""); + } + + @Test + public void addCause_throws_IAE_if_arg_contains_only_spaces() { + Health.Builder builder = newHealthCheckBuilder(); + + expectCauseCannotBeEmptyIAE(); + + builder.addCause(Strings.repeat(" ", 1 + random.nextInt(5))); + } + + @Test + public void getCause_returns_causes_from_builder() { + Health.Builder builder = newHealthCheckBuilder().setStatus(anyStatus); + randomCauses.forEach(builder::addCause); + Health underTest = builder.build(); + + assertThat(underTest.getCauses()) + .isEqualTo(randomCauses); + } + + @Test + public void green_constant() { + assertThat(Health.GREEN).isEqualTo(newHealthCheckBuilder().setStatus(Health.Status.GREEN).build()); + } + + @Test + public void equals_is_based_on_status_and_causes() { + Health.Builder builder1 = newHealthCheckBuilder(); + Health.Builder builder2 = newHealthCheckBuilder(); + + builder1.setStatus(anyStatus); + builder2.setStatus(anyStatus); + randomCauses.forEach(s -> { + builder1.addCause(s); + builder2.addCause(s); + }); + + assertThat(builder1.build()) + .isEqualTo(builder1.build()) + .isEqualTo(builder2.build()) + .isEqualTo(builder2.build()); + } + + @Test + public void not_equals_to_null_nor_other_type() { + assertThat(Health.GREEN).isNotEqualTo(null); + assertThat(Health.GREEN).isNotEqualTo(new Object()); + assertThat(Health.GREEN).isNotEqualTo(Health.Status.GREEN); + } + + @Test + public void hashcode_is_based_on_status_and_causes() { + Health.Builder builder1 = newHealthCheckBuilder(); + Health.Builder builder2 = newHealthCheckBuilder(); + builder1.setStatus(anyStatus); + builder2.setStatus(anyStatus); + randomCauses.forEach(s -> { + builder1.addCause(s); + builder2.addCause(s); + }); + + assertThat(builder1.build().hashCode()) + .isEqualTo(builder1.build().hashCode()) + .isEqualTo(builder2.build().hashCode()) + .isEqualTo(builder2.build().hashCode()); + } + + @Test + public void verify_toString() { + assertThat(Health.GREEN.toString()).isEqualTo("Health{GREEN, causes=[]}"); + Health.Builder builder = newHealthCheckBuilder().setStatus(anyStatus); + randomCauses.forEach(builder::addCause); + + String underTest = builder.build().toString(); + + AbstractCharSequenceAssert<?, String> a = assertThat(underTest) + .describedAs("toString for status %s and causes %s", anyStatus, randomCauses); + if (randomCauses.isEmpty()) { + a.isEqualTo("Health{" + anyStatus + ", causes=[]}"); + } else if (randomCauses.size() == 1) { + a.isEqualTo("Health{" + anyStatus + ", causes=[" + randomCauses.iterator().next() + "]}"); + } else { + a.startsWith("Health{" + anyStatus + ", causes=[") + .endsWith("]}") + .contains(randomCauses); + } + } + + private void expectStatusNotNullNPE() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("status can't be null"); + } + + private void expectCauseCannotBeEmptyIAE() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("cause can't be empty"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/health/WebServerSafemodeCheckTest.java b/server/sonar-server/src/test/java/org/sonar/server/health/WebServerSafemodeCheckTest.java new file mode 100644 index 00000000000..255ed328bc6 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/health/WebServerSafemodeCheckTest.java @@ -0,0 +1,37 @@ +/* + * 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 org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class WebServerSafemodeCheckTest { + private WebServerSafemodeCheck underTest = new WebServerSafemodeCheck(); + + @Test + public void always_returns_RED_status_with_cause() { + Health health = underTest.check(); + + assertThat(health.getStatus()).isEqualTo(Health.Status.RED); + assertThat(health.getCauses()).containsOnly("SonarQube webserver is not up"); + + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/health/WebServerStatusCheckTest.java b/server/sonar-server/src/test/java/org/sonar/server/health/WebServerStatusCheckTest.java new file mode 100644 index 00000000000..8047c5ce3d9 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/health/WebServerStatusCheckTest.java @@ -0,0 +1,96 @@ +/* + * 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.Arrays; +import java.util.Random; +import org.junit.Test; +import org.sonar.server.app.RestartFlagHolder; +import org.sonar.server.platform.Platform; +import org.sonar.server.platform.db.migration.DatabaseMigrationState; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class WebServerStatusCheckTest { + private final DatabaseMigrationState migrationState = mock(DatabaseMigrationState.class); + private final Platform platform = mock(Platform.class); + private final RestartFlagHolder restartFlagHolder = mock(RestartFlagHolder.class); + + private final Random random = new Random(); + + private WebServerStatusCheck underTest = new WebServerStatusCheck(migrationState, platform, restartFlagHolder); + + @Test + public void returns_RED_status_with_cause_if_platform_status_is_not_UP() { + Platform.Status[] statusesButUp = Arrays.stream(Platform.Status.values()) + .filter(s -> s != Platform.Status.UP) + .toArray(Platform.Status[]::new); + Platform.Status randomStatusButUp = statusesButUp[random.nextInt(statusesButUp.length)]; + when(platform.status()).thenReturn(randomStatusButUp); + + Health health = underTest.check(); + + verifyRedHealthWithCause(health); + } + + @Test + public void returns_RED_status_with_cause_if_platform_status_is_UP_but_migrationStatus_is_neither_NONE_nor_SUCCEED() { + when(platform.status()).thenReturn(Platform.Status.UP); + DatabaseMigrationState.Status[] statusesButValidOnes = Arrays.stream(DatabaseMigrationState.Status.values()) + .filter(s -> s != DatabaseMigrationState.Status.NONE) + .filter(s -> s != DatabaseMigrationState.Status.SUCCEEDED) + .toArray(DatabaseMigrationState.Status[]::new); + DatabaseMigrationState.Status randomInvalidStatus = statusesButValidOnes[random.nextInt(statusesButValidOnes.length)]; + when(migrationState.getStatus()).thenReturn(randomInvalidStatus); + + Health health = underTest.check(); + + verifyRedHealthWithCause(health); + } + + @Test + public void returns_RED_with_cause_if_platform_status_is_UP_migration_status_is_valid_but_SQ_is_restarting() { + when(platform.status()).thenReturn(Platform.Status.UP); + when(migrationState.getStatus()).thenReturn(random.nextBoolean() ? DatabaseMigrationState.Status.NONE : DatabaseMigrationState.Status.SUCCEEDED); + when(restartFlagHolder.isRestarting()).thenReturn(true); + + Health health = underTest.check(); + + verifyRedHealthWithCause(health); + } + + @Test + public void returns_GREEN_without_cause_if_platform_status_is_UP_migration_status_is_valid_and_SQ_is_not_restarting() { + when(platform.status()).thenReturn(Platform.Status.UP); + when(migrationState.getStatus()).thenReturn(random.nextBoolean() ? DatabaseMigrationState.Status.NONE : DatabaseMigrationState.Status.SUCCEEDED); + when(restartFlagHolder.isRestarting()).thenReturn(false); + + Health health = underTest.check(); + + assertThat(health).isEqualTo(Health.GREEN); + } + + private void verifyRedHealthWithCause(Health health) { + assertThat(health.getStatus()).isEqualTo(Health.Status.RED); + assertThat(health.getCauses()).containsOnly("SonarQube webserver is not up"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/AbstractHealthActionTestSupport.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/AbstractHealthActionTestSupport.java new file mode 100644 index 00000000000..91985e5f063 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/AbstractHealthActionTestSupport.java @@ -0,0 +1,76 @@ +/* + * 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.Random; +import java.util.stream.IntStream; +import org.apache.commons.lang.RandomStringUtils; +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.WsActionTester; +import org.sonar.test.JsonAssert; +import org.sonarqube.ws.WsSystem; + +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; + +public class AbstractHealthActionTestSupport { + HealthChecker mockedHealthChecker = mock(HealthChecker.class); + + void verifyDefinition(WebService.Action definition) { + 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(); + } + + void verifyExample(WsActionTester underTest) { + when(mockedHealthChecker.check()).thenReturn( + newHealthCheckBuilder() + .setStatus(Health.Status.YELLOW) + .addCause("Elasticsearch status is YELLOW") + .build()); + TestRequest request = underTest.newRequest(); + + JsonAssert.assertJson(request.execute().getInput()) + .isSimilarTo(underTest.getDef().responseExampleAsString()); + } + + void requestReturnsStatusAndCausesFromHealthCheckerCheckMethod(WsActionTester underTest) { + 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(mockedHealthChecker.check()).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()); + } +} 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 new file mode 100644 index 00000000000..32d54bf4b1c --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionModuleTest.java @@ -0,0 +1,69 @@ +/* + * 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.Collection; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Test; +import org.picocontainer.ComponentAdapter; +import org.sonar.core.platform.ComponentContainer; +import org.sonar.server.health.DbConnectionCheck; +import org.sonar.server.health.EsStatusCheck; +import org.sonar.server.health.HealthCheck; +import org.sonar.server.health.HealthCheckerImpl; +import org.sonar.server.health.WebServerStatusCheck; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HealthActionModuleTest { + private HealthActionModule underTest = new HealthActionModule(); + + @Test + public void verify_action_and_HealthChecker() { + ComponentContainer container = new ComponentContainer(); + + underTest.configure(container); + + assertThat(classesAddedToContainer(container)) + .contains(HealthCheckerImpl.class) + .contains(HealthAction.class) + .doesNotContain(SafeModeHealthAction.class); + } + + @Test + public void verify_installed_HealthChecks_implementations() { + ComponentContainer container = new ComponentContainer(); + + underTest.configure(container); + + List<Class<?>> checks = classesAddedToContainer(container).stream().filter(HealthCheck.class::isAssignableFrom).collect(Collectors.toList()); + assertThat(checks) + .hasSize(3) + .contains(WebServerStatusCheck.class) + .contains(DbConnectionCheck.class) + .contains(EsStatusCheck.class); + } + + private List<Class<?>> classesAddedToContainer(ComponentContainer container) { + Collection<ComponentAdapter<?>> componentAdapters = container.getPicoContainer().getComponentAdapters(); + return componentAdapters.stream().map(ComponentAdapter::getComponentImplementation).collect(Collectors.toList()); + } +} 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 new file mode 100644 index 00000000000..201b34fd328 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionTest.java @@ -0,0 +1,93 @@ +/* + * 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.Random; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.server.ws.WebService; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; + +public class HealthActionTest { + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private AbstractHealthActionTestSupport healthActionTestSupport = new AbstractHealthActionTestSupport(); + private WsActionTester underTest = new WsActionTester(new HealthAction(userSessionRule, healthActionTestSupport.mockedHealthChecker)); + + @Test + public void verify_definition() { + WebService.Action definition = underTest.getDef(); + + healthActionTestSupport.verifyDefinition(definition); + } + + @Test + public void execute_fails_with_UnauthorizedException_if_user_is_not_logged_in() { + TestRequest request = underTest.newRequest(); + + expectedException.expect(UnauthorizedException.class); + expectedException.expectMessage("Authentication is required"); + + request.execute(); + } + + @Test + public void execute_fails_with_ForbiddenException_if_user_logged_in_but_not_root() { + TestRequest request = underTest.newRequest(); + userSessionRule.logIn(); + + expectedException.expect(ForbiddenException.class); + expectedException.expectMessage("Insufficient privileges"); + + request.execute(); + } + + @Test + public void verify_example() { + userSessionRule.logIn(); + rootOrSystemAdmin(); + + healthActionTestSupport.verifyExample(underTest); + } + + @Test + public void request_returns_status_and_causes_from_HealthChecker_check_method() { + userSessionRule.logIn(); + rootOrSystemAdmin(); + + healthActionTestSupport.requestReturnsStatusAndCausesFromHealthCheckerCheckMethod(underTest); + } + + private void rootOrSystemAdmin() { + if (new Random().nextBoolean()) { + userSessionRule.setRoot(); + } else { + userSessionRule.setSystemAdministrator(); + } + } +} 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 new file mode 100644 index 00000000000..f99fa201a33 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionModuleTest.java @@ -0,0 +1,70 @@ +/* + * 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.Collection; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Test; +import org.picocontainer.ComponentAdapter; +import org.sonar.core.platform.ComponentContainer; +import org.sonar.server.health.DbConnectionCheck; +import org.sonar.server.health.EsStatusCheck; +import org.sonar.server.health.HealthCheck; +import org.sonar.server.health.HealthCheckerImpl; +import org.sonar.server.health.WebServerSafemodeCheck; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SafeModeHealthActionModuleTest { + private SafeModeHealthActionModule underTest = new SafeModeHealthActionModule(); + + @Test + public void verify_action_and_HealthChecker() { + ComponentContainer container = new ComponentContainer(); + + underTest.configure(container); + + assertThat(classesAddedToContainer(container)) + .contains(HealthCheckerImpl.class) + .contains(SafeModeHealthAction.class) + .doesNotContain(HealthAction.class); + } + + @Test + public void verify_installed_HealthChecks_implementations() { + ComponentContainer container = new ComponentContainer(); + + underTest.configure(container); + + List<Class<?>> checks = classesAddedToContainer(container).stream().filter(HealthCheck.class::isAssignableFrom).collect(Collectors.toList()); + assertThat(checks) + .hasSize(3) + .contains(WebServerSafemodeCheck.class) + .contains(DbConnectionCheck.class) + .contains(EsStatusCheck.class); + } + + private List<Class<?>> classesAddedToContainer(ComponentContainer container) { + Collection<ComponentAdapter<?>> componentAdapters = container.getPicoContainer().getComponentAdapters(); + return componentAdapters.stream().map(ComponentAdapter::getComponentImplementation).collect(Collectors.toList()); + } + +} 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 00000000000..0cec5200305 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionTest.java @@ -0,0 +1,47 @@ +/* + * 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.junit.Test; +import org.sonar.api.server.ws.WebService; +import org.sonar.server.ws.WsActionTester; + +public class SafeModeHealthActionTest { + private AbstractHealthActionTestSupport healthActionTestSupport = new AbstractHealthActionTestSupport(); + private WsActionTester underTest = new WsActionTester(new SafeModeHealthAction(healthActionTestSupport.mockedHealthChecker)); + + @Test + public void verify_definition() { + WebService.Action definition = underTest.getDef(); + + healthActionTestSupport.verifyDefinition(definition); + } + + @Test + public void verify_example() { + healthActionTestSupport.verifyExample(underTest); + } + + @Test + public void request_returns_status_and_causes_from_HealthChecker_check_method() { + healthActionTestSupport.requestReturnsStatusAndCausesFromHealthCheckerCheckMethod(underTest); + } + +} diff --git a/sonar-ws/src/main/protobuf/ws-system.proto b/sonar-ws/src/main/protobuf/ws-system.proto new file mode 100644 index 00000000000..4f0fd9af5fc --- /dev/null +++ b/sonar-ws/src/main/protobuf/ws-system.proto @@ -0,0 +1,41 @@ +// SonarQube, open source software quality management tool. +// Copyright (C) 2008-2016 SonarSource +// mailto:contact AT sonarsource DOT com +// +// SonarQube 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. +// +// SonarQube 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. + +syntax = "proto2"; + +package sonarqube.ws.system; + +option java_package = "org.sonarqube.ws"; +option java_outer_classname = "WsSystem"; +option optimize_for = SPEED; + +// GET api/system/health +message HealthResponse { + optional Health health = 1; + repeated Cause causes = 2; +} + +message Cause { + optional string message = 1; +} + +enum Health { + GREEN = 0; + YELLOW = 1; + RED = 2; +} |