diff options
52 files changed, 1019 insertions, 1115 deletions
diff --git a/server/sonar-process/src/main/java/org/sonar/process/systeminfo/JvmStateSection.java b/server/sonar-process/src/main/java/org/sonar/process/systeminfo/JvmStateSection.java index 575083280b1..656fba7e75b 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/systeminfo/JvmStateSection.java +++ b/server/sonar-process/src/main/java/org/sonar/process/systeminfo/JvmStateSection.java @@ -57,8 +57,11 @@ public class JvmStateSection implements SystemInfoSection { addAttributeInMb(protobuf, "Non Heap Init (MB)", nonHeap.getInit()); addAttributeInMb(protobuf, "Non Heap Max (MB)", nonHeap.getMax()); addAttributeInMb(protobuf, "Non Heap Used (MB)", nonHeap.getUsed()); + ThreadMXBean thread = ManagementFactory.getThreadMXBean(); - setAttribute(protobuf, "Thread Count", thread.getThreadCount()); + setAttribute(protobuf, "Threads", thread.getThreadCount()); + + setAttribute(protobuf,"Processors", Runtime.getRuntime().availableProcessors()); return protobuf.build(); } diff --git a/server/sonar-process/src/test/java/org/sonar/process/systeminfo/JvmStateSectionTest.java b/server/sonar-process/src/test/java/org/sonar/process/systeminfo/JvmStateSectionTest.java index 5276a524353..3057e9933e9 100644 --- a/server/sonar-process/src/test/java/org/sonar/process/systeminfo/JvmStateSectionTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/systeminfo/JvmStateSectionTest.java @@ -30,7 +30,7 @@ import static org.mockito.Mockito.when; public class JvmStateSectionTest { - public static final String PROCESS_NAME = "the process name"; + private static final String PROCESS_NAME = "the process name"; @Test public void toSystemInfoSection() { @@ -39,7 +39,7 @@ public class JvmStateSectionTest { assertThat(section.getName()).isEqualTo(PROCESS_NAME); assertThat(section.getAttributesCount()).isGreaterThan(0); - assertThat(section.getAttributesList()).extracting("key").contains("Thread Count"); + assertThat(section.getAttributesList()).extracting("key").contains("Threads", "Processors"); } @Test diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSectionMBean.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSectionMBean.java deleted file mode 100644 index 6358f68593f..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSectionMBean.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.monitoring; - -/** - * The public attributes of {@link EsSection} - * to be exported in JMX bean. - */ -public interface EsSectionMBean { - String getState(); -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSection.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java index 2e86e6ef854..d6f5f0df07d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSection.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java @@ -19,44 +19,28 @@ */ package org.sonar.server.platform.monitoring; -import java.util.Map; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.admin.cluster.node.stats.NodeStats; import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse; import org.elasticsearch.action.admin.cluster.stats.ClusterStatsResponse; -import org.elasticsearch.action.admin.indices.stats.IndexStats; -import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.breaker.CircuitBreaker; import org.sonar.api.utils.log.Loggers; +import org.sonar.process.systeminfo.SystemInfoSection; import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; import org.sonar.server.es.EsClient; import static org.apache.commons.io.FileUtils.byteCountToDisplaySize; import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; -public class EsSection extends BaseSectionMBean implements EsSectionMBean { +public class EsStateSection implements SystemInfoSection { private final EsClient esClient; - public EsSection(EsClient esClient) { + public EsStateSection(EsClient esClient) { this.esClient = esClient; } - @Override - public String name() { - return "Elasticsearch"; - } - - /** - * MXBean does not allow to return enum {@link ClusterHealthStatus}, so - * returning String. - */ - @Override - public String getState() { - return getStateAsEnum().name(); - } - private ClusterHealthStatus getStateAsEnum() { return clusterStats().getStatus(); } @@ -64,29 +48,17 @@ public class EsSection extends BaseSectionMBean implements EsSectionMBean { @Override public ProtobufSystemInfo.Section toProtobuf() { ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); - protobuf.setName(name()); + protobuf.setName("Search State"); try { setAttribute(protobuf, "State", getStateAsEnum().name()); completeNodeAttributes(protobuf); - completeIndexAttributes(protobuf); - - } catch (Exception es) { - Loggers.get(EsSection.class).warn("Failed to retrieve ES attributes. There will be only a single \"state\" attribute.", es); + } catch (Exception es) { + Loggers.get(EsStateSection.class).warn("Failed to retrieve ES attributes. There will be only a single \"state\" attribute.", es); setAttribute(protobuf, "State", es.getCause() instanceof ElasticsearchException ? es.getCause().getMessage() : es.getMessage()); } return protobuf.build(); } - private void completeIndexAttributes(ProtobufSystemInfo.Section.Builder protobuf) { - IndicesStatsResponse indicesStats = esClient.prepareStats().all().get(); - for (Map.Entry<String, IndexStats> indexStats : indicesStats.getIndices().entrySet()) { - String prefix = "Index " + indexStats.getKey() + " - "; - setAttribute(protobuf, prefix + "Docs", indexStats.getValue().getPrimaries().getDocs().getCount()); - setAttribute(protobuf, prefix + "Shards", indexStats.getValue().getShards().length); - setAttribute(protobuf, prefix + "Store Size", byteCountToDisplaySize(indexStats.getValue().getPrimaries().getStore().getSizeInBytes())); - } - } - private void completeNodeAttributes(ProtobufSystemInfo.Section.Builder protobuf) { NodesStatsResponse nodesStats = esClient.prepareNodesStats().all().get(); if (!nodesStats.getNodes().isEmpty()) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStatisticsSection.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStatisticsSection.java new file mode 100644 index 00000000000..12656ae4f3e --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStatisticsSection.java @@ -0,0 +1,64 @@ +/* + * 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.monitoring; + +import java.util.Map; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.admin.indices.stats.IndexStats; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; +import org.sonar.api.utils.log.Loggers; +import org.sonar.process.systeminfo.SystemInfoSection; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.es.EsClient; + +import static org.apache.commons.io.FileUtils.byteCountToDisplaySize; +import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; + +public class EsStatisticsSection implements SystemInfoSection { + + private final EsClient esClient; + + public EsStatisticsSection(EsClient esClient) { + this.esClient = esClient; + } + + @Override + public ProtobufSystemInfo.Section toProtobuf() { + ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); + protobuf.setName("Search Statistics"); + try { + completeIndexAttributes(protobuf); + } catch (Exception es) { + Loggers.get(EsStatisticsSection.class).warn("Failed to retrieve ES attributes. There will be only a single \"Error\" attribute.", es); + setAttribute(protobuf, "Error", es.getCause() instanceof ElasticsearchException ? es.getCause().getMessage() : es.getMessage()); + } + return protobuf.build(); + } + + private void completeIndexAttributes(ProtobufSystemInfo.Section.Builder protobuf) { + IndicesStatsResponse indicesStats = esClient.prepareStats().all().get(); + for (Map.Entry<String, IndexStats> indexStats : indicesStats.getIndices().entrySet()) { + String prefix = "Index " + indexStats.getKey() + " - "; + setAttribute(protobuf, prefix + "Docs", indexStats.getValue().getPrimaries().getDocs().getCount()); + setAttribute(protobuf, prefix + "Shards", indexStats.getValue().getShards().length); + setAttribute(protobuf, prefix + "Store Size", byteCountToDisplaySize(indexStats.getValue().getPrimaries().getStore().getSizeInBytes())); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSection.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSection.java deleted file mode 100644 index ead5a7475fd..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSection.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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.monitoring; - -import com.google.common.base.Joiner; -import java.io.File; -import java.util.List; -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; -import org.sonar.api.CoreProperties; -import org.sonar.api.config.Configuration; -import org.sonar.api.platform.Server; -import org.sonar.api.security.SecurityRealm; -import org.sonar.api.server.authentication.IdentityProvider; -import org.sonar.core.util.stream.MoreCollectors; -import org.sonar.process.ProcessProperties; -import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; -import org.sonar.server.authentication.IdentityProviderRepository; -import org.sonar.server.health.Health; -import org.sonar.server.health.HealthChecker; -import org.sonar.server.platform.ServerIdLoader; -import org.sonar.server.platform.ServerLogging; -import org.sonar.server.user.SecurityRealmFactory; - -import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; - -public class SonarQubeSection extends BaseSectionMBean implements SonarQubeSectionMBean { - - private static final Joiner COMMA_JOINER = Joiner.on(", "); - - static final String BRANDING_FILE_PATH = "web/WEB-INF/classes/com/sonarsource/branding"; - - private final Configuration config; - private final SecurityRealmFactory securityRealmFactory; - private final IdentityProviderRepository identityProviderRepository; - private final Server server; - private final ServerLogging serverLogging; - private final ServerIdLoader serverIdLoader; - private final HealthChecker healthChecker; - - public SonarQubeSection(Configuration config, SecurityRealmFactory securityRealmFactory, - IdentityProviderRepository identityProviderRepository, Server server, ServerLogging serverLogging, - ServerIdLoader serverIdLoader, HealthChecker healthChecker) { - this.config = config; - this.securityRealmFactory = securityRealmFactory; - this.identityProviderRepository = identityProviderRepository; - this.server = server; - this.serverLogging = serverLogging; - this.serverIdLoader = serverIdLoader; - this.healthChecker = healthChecker; - } - - @Override - public String getServerId() { - return serverIdLoader.getRaw().orElse(null); - } - - @Override - public String getVersion() { - return server.getVersion(); - } - - @Override - public String getLogLevel() { - return serverLogging.getRootLoggerLevel().name(); - } - - @CheckForNull - private String getExternalUserAuthentication() { - SecurityRealm realm = securityRealmFactory.getRealm(); - return realm == null ? null : realm.getName(); - } - - private List<String> getEnabledIdentityProviders() { - return identityProviderRepository.getAllEnabledAndSorted() - .stream() - .filter(IdentityProvider::isEnabled) - .map(IdentityProvider::getName) - .collect(MoreCollectors.toList()); - } - - private List<String> getAllowsToSignUpEnabledIdentityProviders() { - return identityProviderRepository.getAllEnabledAndSorted() - .stream() - .filter(IdentityProvider::isEnabled) - .filter(IdentityProvider::allowsUsersToSignUp) - .map(IdentityProvider::getName) - .collect(MoreCollectors.toList()); - } - - private boolean getForceAuthentication() { - return config.getBoolean(CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY).orElse(false); - } - - private boolean isOfficialDistribution() { - // the dependency com.sonarsource:sonarsource-branding is shaded to webapp - // during release (see sonar-web pom) - File brandingFile = new File(server.getRootDir(), BRANDING_FILE_PATH); - // no need to check that the file exists. java.io.File#length() returns zero in this case. - return brandingFile.length() > 0L; - } - - @Override - public String name() { - return "SonarQube"; - } - - @Override - public ProtobufSystemInfo.Section toProtobuf() { - ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); - protobuf.setName(name()); - - serverIdLoader.get().ifPresent(serverId -> { - setAttribute(protobuf, "Server ID", serverId.getId()); - setAttribute(protobuf, "Server ID validated", serverId.isValid()); - }); - Health health = healthChecker.checkNode(); - setAttribute(protobuf, "Health", health.getStatus().name()); - setAttribute(protobuf, "Health Causes", health.getCauses()); - setAttribute(protobuf, "Version", getVersion()); - setAttribute(protobuf, "External User Authentication", getExternalUserAuthentication()); - addIfNotEmpty(protobuf, "Accepted external identity providers", getEnabledIdentityProviders()); - addIfNotEmpty(protobuf, "External identity providers whose users are allowed to sign themselves up", getAllowsToSignUpEnabledIdentityProviders()); - setAttribute(protobuf, "Force authentication", getForceAuthentication()); - setAttribute(protobuf, "Official Distribution", isOfficialDistribution()); - setAttribute(protobuf, "Home Dir", config.get(ProcessProperties.PATH_HOME).orElse(null)); - setAttribute(protobuf, "Data Dir", config.get(ProcessProperties.PATH_DATA).orElse(null)); - setAttribute(protobuf, "Temp Dir", config.get(ProcessProperties.PATH_TEMP).orElse(null)); - setAttribute(protobuf, "Logs Dir", config.get(ProcessProperties.PATH_LOGS).orElse(null)); - setAttribute(protobuf, "Logs Level", getLogLevel()); - return protobuf.build(); - } - - private static void addIfNotEmpty(ProtobufSystemInfo.Section.Builder protobuf, String key, @Nullable List<String> values) { - if (values != null && !values.isEmpty()) { - setAttribute(protobuf, key, COMMA_JOINER.join(values)); - } - } -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSection.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSection.java index bbd623f52b9..d19f7162ae4 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSection.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSection.java @@ -19,85 +19,140 @@ */ package org.sonar.server.platform.monitoring; -import java.lang.management.ClassLoadingMXBean; -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryMXBean; -import java.lang.management.RuntimeMXBean; -import java.lang.management.ThreadMXBean; -import java.util.Date; -import org.sonar.api.utils.System2; -import org.sonar.process.systeminfo.SystemInfoSection; +import com.google.common.base.Joiner; +import java.io.File; +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.CoreProperties; +import org.sonar.api.config.Configuration; +import org.sonar.api.platform.Server; +import org.sonar.api.security.SecurityRealm; +import org.sonar.api.server.authentication.IdentityProvider; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.process.ProcessProperties; import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.authentication.IdentityProviderRepository; +import org.sonar.server.health.Health; +import org.sonar.server.health.HealthChecker; +import org.sonar.server.platform.ServerIdLoader; +import org.sonar.server.platform.ServerLogging; +import org.sonar.server.user.SecurityRealmFactory; -import static java.lang.String.format; -import static org.sonar.api.utils.DateUtils.formatDateTime; import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; -/** - * JVM runtime information. Not exported as a MXBean because these information - * are natively provided. - */ -public class SystemSection implements SystemInfoSection { - private final System2 system; +public class SystemSection extends BaseSectionMBean implements SystemSectionMBean { + + private static final Joiner COMMA_JOINER = Joiner.on(", "); + + static final String BRANDING_FILE_PATH = "web/WEB-INF/classes/com/sonarsource/branding"; - public SystemSection() { - this(System2.INSTANCE); + private final Configuration config; + private final SecurityRealmFactory securityRealmFactory; + private final IdentityProviderRepository identityProviderRepository; + private final Server server; + private final ServerLogging serverLogging; + private final ServerIdLoader serverIdLoader; + private final HealthChecker healthChecker; + + public SystemSection(Configuration config, SecurityRealmFactory securityRealmFactory, + IdentityProviderRepository identityProviderRepository, Server server, ServerLogging serverLogging, + ServerIdLoader serverIdLoader, HealthChecker healthChecker) { + this.config = config; + this.securityRealmFactory = securityRealmFactory; + this.identityProviderRepository = identityProviderRepository; + this.server = server; + this.serverLogging = serverLogging; + this.serverIdLoader = serverIdLoader; + this.healthChecker = healthChecker; } - SystemSection(System2 system) { - this.system = system; + @Override + public String getServerId() { + return serverIdLoader.getRaw().orElse(null); } @Override - public ProtobufSystemInfo.Section toProtobuf() { - ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); - protobuf.setName("System"); + public String getVersion() { + return server.getVersion(); + } - setAttribute(protobuf, "System Date", formatDateTime(new Date(system.now()))); - setAttribute(protobuf, "Start Time", formatDateTime(new Date(runtimeMXBean().getStartTime()))); - setAttribute(protobuf, "JVM Vendor", runtimeMXBean().getVmVendor()); - setAttribute(protobuf, "JVM Name", runtimeMXBean().getVmName()); - setAttribute(protobuf, "JVM Version", runtimeMXBean().getVmVersion()); - setAttribute(protobuf, "Processors", runtime().availableProcessors()); - setAttribute(protobuf, "System Classpath", runtimeMXBean().getClassPath()); - setAttribute(protobuf, "BootClassPath", runtimeMXBean().getBootClassPath()); - setAttribute(protobuf, "Library Path", runtimeMXBean().getLibraryPath()); - setAttribute(protobuf, "Total Memory", formatMemory(runtime().totalMemory())); - setAttribute(protobuf, "Free Memory", formatMemory(runtime().freeMemory())); - setAttribute(protobuf, "Max Memory", formatMemory(runtime().maxMemory())); - setAttribute(protobuf, "Heap", memoryMXBean().getHeapMemoryUsage().toString()); - setAttribute(protobuf, "Non Heap", memoryMXBean().getNonHeapMemoryUsage().toString()); - setAttribute(protobuf, "System Load Average", format("%.1f%% (last minute)", ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage() * 100.0)); - setAttribute(protobuf, "Loaded Classes", classLoadingMXBean().getLoadedClassCount()); - setAttribute(protobuf, "Total Loaded Classes", classLoadingMXBean().getTotalLoadedClassCount()); - setAttribute(protobuf, "Unloaded Classes", classLoadingMXBean().getUnloadedClassCount()); - setAttribute(protobuf, "Threads", threadMXBean().getThreadCount()); - setAttribute(protobuf, "Threads Peak", threadMXBean().getPeakThreadCount()); - setAttribute(protobuf, "Daemon Thread", threadMXBean().getDaemonThreadCount()); - return protobuf.build(); + @Override + public String getLogLevel() { + return serverLogging.getRootLoggerLevel().name(); } - private static RuntimeMXBean runtimeMXBean() { - return ManagementFactory.getRuntimeMXBean(); + @CheckForNull + private String getExternalUserAuthentication() { + SecurityRealm realm = securityRealmFactory.getRealm(); + return realm == null ? null : realm.getName(); } - private static Runtime runtime() { - return Runtime.getRuntime(); + private List<String> getEnabledIdentityProviders() { + return identityProviderRepository.getAllEnabledAndSorted() + .stream() + .filter(IdentityProvider::isEnabled) + .map(IdentityProvider::getName) + .collect(MoreCollectors.toList()); } - private static MemoryMXBean memoryMXBean() { - return ManagementFactory.getMemoryMXBean(); + private List<String> getAllowsToSignUpEnabledIdentityProviders() { + return identityProviderRepository.getAllEnabledAndSorted() + .stream() + .filter(IdentityProvider::isEnabled) + .filter(IdentityProvider::allowsUsersToSignUp) + .map(IdentityProvider::getName) + .collect(MoreCollectors.toList()); } - private static ClassLoadingMXBean classLoadingMXBean() { - return ManagementFactory.getClassLoadingMXBean(); + private boolean getForceAuthentication() { + return config.getBoolean(CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY).orElse(false); } - private static ThreadMXBean threadMXBean() { - return ManagementFactory.getThreadMXBean(); + private boolean isOfficialDistribution() { + // the dependency com.sonarsource:sonarsource-branding is shaded to webapp + // during release (see sonar-web pom) + File brandingFile = new File(server.getRootDir(), BRANDING_FILE_PATH); + // no need to check that the file exists. java.io.File#length() returns zero in this case. + return brandingFile.length() > 0L; + } + + @Override + public String name() { + // JMX name + return "SonarQube"; + } + + @Override + public ProtobufSystemInfo.Section toProtobuf() { + ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); + protobuf.setName("System"); + + serverIdLoader.get().ifPresent(serverId -> { + setAttribute(protobuf, "Server ID", serverId.getId()); + setAttribute(protobuf, "Server ID validated", serverId.isValid()); + }); + Health health = healthChecker.checkNode(); + setAttribute(protobuf, "Health", health.getStatus().name()); + setAttribute(protobuf, "Health Causes", health.getCauses()); + setAttribute(protobuf, "Version", getVersion()); + setAttribute(protobuf, "External User Authentication", getExternalUserAuthentication()); + addIfNotEmpty(protobuf, "Accepted external identity providers", getEnabledIdentityProviders()); + addIfNotEmpty(protobuf, "External identity providers whose users are allowed to sign themselves up", getAllowsToSignUpEnabledIdentityProviders()); + setAttribute(protobuf, "High Availability", false); + setAttribute(protobuf, "Official Distribution", isOfficialDistribution()); + setAttribute(protobuf, "Force authentication", getForceAuthentication()); + setAttribute(protobuf, "Home Dir", config.get(ProcessProperties.PATH_HOME).orElse(null)); + setAttribute(protobuf, "Data Dir", config.get(ProcessProperties.PATH_DATA).orElse(null)); + setAttribute(protobuf, "Temp Dir", config.get(ProcessProperties.PATH_TEMP).orElse(null)); + setAttribute(protobuf, "Logs Dir", config.get(ProcessProperties.PATH_LOGS).orElse(null)); + setAttribute(protobuf, "Logs Level", getLogLevel()); + return protobuf.build(); } - private static String formatMemory(long memoryInBytes) { - return format("%d MB", memoryInBytes / 1_000_000); + private static void addIfNotEmpty(ProtobufSystemInfo.Section.Builder protobuf, String key, @Nullable List<String> values) { + if (values != null && !values.isEmpty()) { + setAttribute(protobuf, key, COMMA_JOINER.join(values)); + } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSectionMBean.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.java index 1a562a6bc30..7c3f8f01aa9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSectionMBean.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.java @@ -21,7 +21,7 @@ package org.sonar.server.platform.monitoring; import javax.annotation.CheckForNull; -public interface SonarQubeSectionMBean { +public interface SystemSectionMBean { @CheckForNull String getServerId(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/WebSystemInfoModule.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/WebSystemInfoModule.java index c565219240f..09c40749fc8 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/WebSystemInfoModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/WebSystemInfoModule.java @@ -35,10 +35,10 @@ public class WebSystemInfoModule { new JvmPropertiesSection("Web JVM Properties"), new JvmStateSection("Web JVM State"), DatabaseSection.class, - EsSection.class, + EsStateSection.class, + EsStatisticsSection.class, PluginsSection.class, SettingsSection.class, - SonarQubeSection.class, SystemSection.class, StandaloneInfoAction.class @@ -50,7 +50,7 @@ public class WebSystemInfoModule { new JvmPropertiesSection("Web JVM Properties"), new JvmStateSection("Web JVM State"), DatabaseSection.class, - EsSection.class, + EsStateSection.class, PluginsSection.class, ClusterInfoAction.class diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ClusterInfoAction.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ClusterInfoAction.java index 6f9125f40cc..d298d99c03a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ClusterInfoAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ClusterInfoAction.java @@ -19,6 +19,7 @@ */ package org.sonar.server.platform.ws; +import java.util.Arrays; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; @@ -50,14 +51,16 @@ public class ClusterInfoAction implements SystemWsAction { try (JsonWriter json = response.newJsonWriter()) { json.beginObject(); - // global section - json.prop("Cluster", true); - json.prop("Cluster Name", "foo"); + json.name("System"); + json.beginObject(); + json.prop("High Availability", true); + json.prop("Cluster Name", "fooo"); json.prop("Server Id", "ABC123"); json.prop("Health", "RED"); json .name("Health Causes") - .beginArray().beginObject().prop("message", "Requires at least two search nodes").endObject().endArray(); + .beginArray().values(Arrays.asList("foo", "bar")).endArray(); + json.endObject(); json.name("Settings"); json.beginObject(); @@ -65,8 +68,6 @@ public class ClusterInfoAction implements SystemWsAction { json.prop("sonar.externalIdentityProviders", "GitHub, BitBucket"); json.endObject(); - - json.name("Database"); json .beginObject() @@ -83,7 +84,7 @@ public class ClusterInfoAction implements SystemWsAction { .prop("workersPerNode", 4) .endObject(); - json.name("Elasticsearch"); + json.name("Search"); json .beginObject() .prop("Health", "GREEN") diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsSectionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.java index a55ae819df3..af248d63640 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsSectionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.java @@ -35,21 +35,20 @@ import static org.mockito.Mockito.when; import static org.sonar.process.systeminfo.SystemInfoUtils.attribute; import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs; -public class EsSectionTest { +public class EsStateSectionTest { @Rule public EsTester esTester = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig())); - private EsSection underTest = new EsSection(esTester.client()); + private EsStateSection underTest = new EsStateSection(esTester.client()); @Test public void name() { - assertThat(underTest.name()).isEqualTo("Elasticsearch"); + assertThat(underTest.toProtobuf().getName()).isEqualTo("Search State"); } @Test public void es_state() { - assertThat(underTest.getState()).isEqualTo(ClusterHealthStatus.GREEN.name()); assertThatAttributeIs(underTest.toProtobuf(), "State", ClusterHealthStatus.GREEN.name()); } @@ -60,19 +59,9 @@ public class EsSectionTest { } @Test - public void index_attributes() { - ProtobufSystemInfo.Section section = underTest.toProtobuf(); - - // one index "issues" - assertThat(attribute(section, "Index issues - Docs").getLongValue()).isEqualTo(0L); - assertThat(attribute(section, "Index issues - Shards").getLongValue()).isGreaterThan(0); - assertThat(attribute(section, "Index issues - Store Size").getStringValue()).isNotNull(); - } - - @Test public void attributes_displays_exception_message_when_cause_null_when_client_fails() { EsClient esClientMock = mock(EsClient.class); - EsSection underTest = new EsSection(esClientMock); + EsStateSection underTest = new EsStateSection(esClientMock); when(esClientMock.prepareClusterStats()).thenThrow(new RuntimeException("RuntimeException with no cause")); ProtobufSystemInfo.Section section = underTest.toProtobuf(); @@ -82,7 +71,7 @@ public class EsSectionTest { @Test public void attributes_displays_exception_message_when_cause_is_not_ElasticSearchException_when_client_fails() { EsClient esClientMock = mock(EsClient.class); - EsSection underTest = new EsSection(esClientMock); + EsStateSection underTest = new EsStateSection(esClientMock); when(esClientMock.prepareClusterStats()).thenThrow(new RuntimeException("RuntimeException with cause not ES", new IllegalArgumentException("some cause message"))); ProtobufSystemInfo.Section section = underTest.toProtobuf(); @@ -92,7 +81,7 @@ public class EsSectionTest { @Test public void attributes_displays_cause_message_when_cause_is_ElasticSearchException_when_client_fails() { EsClient esClientMock = mock(EsClient.class); - EsSection underTest = new EsSection(esClientMock); + EsStateSection underTest = new EsStateSection(esClientMock); when(esClientMock.prepareClusterStats()).thenThrow(new RuntimeException("RuntimeException with ES cause", new ElasticsearchException("some cause message"))); ProtobufSystemInfo.Section section = underTest.toProtobuf(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStatisticsSectionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStatisticsSectionTest.java new file mode 100644 index 00000000000..44c583ebb2e --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStatisticsSectionTest.java @@ -0,0 +1,88 @@ +/* + * 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.monitoring; + +import org.elasticsearch.ElasticsearchException; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.es.EsClient; +import org.sonar.server.es.EsTester; +import org.sonar.server.issue.index.IssueIndexDefinition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.process.systeminfo.SystemInfoUtils.attribute; +import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs; + +public class EsStatisticsSectionTest { + + @Rule + public EsTester esTester = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig())); + + private EsStatisticsSection underTest = new EsStatisticsSection(esTester.client()); + + @Test + public void name() { + assertThat(underTest.toProtobuf().getName()).isEqualTo("Search Statistics"); + } + + @Test + public void index_attributes() { + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + + // one index "issues" + assertThat(attribute(section, "Index issues - Docs").getLongValue()).isEqualTo(0L); + assertThat(attribute(section, "Index issues - Shards").getLongValue()).isGreaterThan(0); + assertThat(attribute(section, "Index issues - Store Size").getStringValue()).isNotNull(); + } + + @Test + public void attributes_displays_exception_message_when_cause_null_when_client_fails() { + EsClient esClientMock = mock(EsClient.class); + EsStatisticsSection underTest = new EsStatisticsSection(esClientMock); + when(esClientMock.prepareStats()).thenThrow(new RuntimeException("RuntimeException with no cause")); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + assertThatAttributeIs(section, "Error", "RuntimeException with no cause"); + } + + @Test + public void attributes_displays_exception_message_when_cause_is_not_ElasticSearchException_when_client_fails() { + EsClient esClientMock = mock(EsClient.class); + EsStatisticsSection underTest = new EsStatisticsSection(esClientMock); + when(esClientMock.prepareStats()).thenThrow(new RuntimeException("RuntimeException with cause not ES", new IllegalArgumentException("some cause message"))); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + assertThatAttributeIs(section, "Error", "RuntimeException with cause not ES"); + } + + @Test + public void attributes_displays_cause_message_when_cause_is_ElasticSearchException_when_client_fails() { + EsClient esClientMock = mock(EsClient.class); + EsStatisticsSection underTest = new EsStatisticsSection(esClientMock); + when(esClientMock.prepareStats()).thenThrow(new RuntimeException("RuntimeException with ES cause", new ElasticsearchException("some cause message"))); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + assertThatAttributeIs(section, "Error", "some cause message"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SonarQubeSectionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SonarQubeSectionTest.java deleted file mode 100644 index fac46e81c31..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SonarQubeSectionTest.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * 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.monitoring; - -import java.io.File; -import java.util.Optional; -import org.apache.commons.io.FileUtils; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.sonar.api.config.internal.MapSettings; -import org.sonar.api.platform.Server; -import org.sonar.api.security.SecurityRealm; -import org.sonar.api.utils.log.LoggerLevel; -import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; -import org.sonar.server.authentication.IdentityProviderRepositoryRule; -import org.sonar.server.authentication.TestIdentityProvider; -import org.sonar.server.health.Health; -import org.sonar.server.health.TestStandaloneHealthChecker; -import org.sonar.server.platform.ServerId; -import org.sonar.server.platform.ServerIdLoader; -import org.sonar.server.platform.ServerLogging; -import org.sonar.server.user.SecurityRealmFactory; - -import static java.util.Arrays.asList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.sonar.process.systeminfo.SystemInfoUtils.attribute; -import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs; - -public class SonarQubeSectionTest { - - private static final String SERVER_ID_PROPERTY = "Server ID"; - private static final String SERVER_ID_VALIDATED_PROPERTY = "Server ID validated"; - - @Rule - public TemporaryFolder temp = new TemporaryFolder(); - - @Rule - public IdentityProviderRepositoryRule identityProviderRepository = new IdentityProviderRepositoryRule(); - - private MapSettings settings = new MapSettings(); - private Server server = mock(Server.class); - private ServerIdLoader serverIdLoader = mock(ServerIdLoader.class); - private ServerLogging serverLogging = mock(ServerLogging.class); - private SecurityRealmFactory securityRealmFactory = mock(SecurityRealmFactory.class); - private TestStandaloneHealthChecker healthChecker = new TestStandaloneHealthChecker(); - - private SonarQubeSection underTest = new SonarQubeSection(settings.asConfig(), securityRealmFactory, identityProviderRepository, server, - serverLogging, serverIdLoader, healthChecker); - - @Before - public void setUp() throws Exception { - when(serverLogging.getRootLoggerLevel()).thenReturn(LoggerLevel.DEBUG); - when(serverIdLoader.getRaw()).thenReturn(Optional.empty()); - when(serverIdLoader.get()).thenReturn(Optional.empty()); - } - - @Test - public void name_is_not_empty() { - assertThat(underTest.name()).isNotEmpty(); - } - - @Test - public void test_getServerId() { - when(serverIdLoader.getRaw()).thenReturn(Optional.of("ABC")); - assertThat(underTest.getServerId()).isEqualTo("ABC"); - - when(serverIdLoader.getRaw()).thenReturn(Optional.empty()); - assertThat(underTest.getServerId()).isNull(); - } - - @Test - public void attributes_contain_information_about_valid_server_id() { - when(serverIdLoader.get()).thenReturn(Optional.of(new ServerId("ABC", true))); - - ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); - assertThatAttributeIs(protobuf, SERVER_ID_PROPERTY, "ABC"); - assertThatAttributeIs(protobuf, SERVER_ID_VALIDATED_PROPERTY, true); - } - - @Test - public void attributes_contain_information_about_non_valid_server_id() { - when(serverIdLoader.get()).thenReturn(Optional.of(new ServerId("ABC", false))); - - ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); - assertThatAttributeIs(protobuf, SERVER_ID_PROPERTY, "ABC"); - assertThatAttributeIs(protobuf, SERVER_ID_VALIDATED_PROPERTY, false); - } - - @Test - public void attributes_do_not_contain_information_about_server_id_if_absent() { - when(serverIdLoader.get()).thenReturn(Optional.empty()); - - ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); - assertThat(attribute(protobuf, SERVER_ID_PROPERTY)).isNull(); - assertThat(attribute(protobuf, SERVER_ID_VALIDATED_PROPERTY)).isNull(); - } - - @Test - public void official_distribution() throws Exception { - File rootDir = temp.newFolder(); - FileUtils.write(new File(rootDir, SonarQubeSection.BRANDING_FILE_PATH), "1.2"); - - when(server.getRootDir()).thenReturn(rootDir); - - ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); - assertThatAttributeIs(protobuf, "Official Distribution", true); - } - - @Test - public void not_an_official_distribution() throws Exception { - File rootDir = temp.newFolder(); - // branding file is missing - when(server.getRootDir()).thenReturn(rootDir); - - ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); - assertThatAttributeIs(protobuf, "Official Distribution", false); - } - - @Test - public void get_log_level() throws Exception { - ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); - assertThatAttributeIs(protobuf, "Logs Level", "DEBUG"); - } - - @Test - public void get_realm() throws Exception { - SecurityRealm realm = mock(SecurityRealm.class); - when(realm.getName()).thenReturn("LDAP"); - when(securityRealmFactory.getRealm()).thenReturn(realm); - - ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); - assertThatAttributeIs(protobuf, "External User Authentication", "LDAP"); - } - - @Test - public void no_realm() throws Exception { - when(securityRealmFactory.getRealm()).thenReturn(null); - - ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); - assertThat(attribute(protobuf, "External User Authentication")).isNull(); - } - - @Test - public void get_enabled_identity_providers() throws Exception { - identityProviderRepository.addIdentityProvider(new TestIdentityProvider() - .setKey("github") - .setName("GitHub") - .setEnabled(true)); - identityProviderRepository.addIdentityProvider(new TestIdentityProvider() - .setKey("bitbucket") - .setName("Bitbucket") - .setEnabled(true)); - identityProviderRepository.addIdentityProvider(new TestIdentityProvider() - .setKey("disabled") - .setName("Disabled") - .setEnabled(false)); - - ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); - assertThatAttributeIs(protobuf, "Accepted external identity providers", "Bitbucket, GitHub"); - } - - @Test - public void get_enabled_identity_providers_allowing_users_to_signup() throws Exception { - identityProviderRepository.addIdentityProvider(new TestIdentityProvider() - .setKey("github") - .setName("GitHub") - .setEnabled(true) - .setAllowsUsersToSignUp(true)); - identityProviderRepository.addIdentityProvider(new TestIdentityProvider() - .setKey("bitbucket") - .setName("Bitbucket") - .setEnabled(true) - .setAllowsUsersToSignUp(false)); - identityProviderRepository.addIdentityProvider(new TestIdentityProvider() - .setKey("disabled") - .setName("Disabled") - .setEnabled(false) - .setAllowsUsersToSignUp(true)); - - ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); - assertThatAttributeIs(protobuf, "External identity providers whose users are allowed to sign themselves up", "GitHub"); - } - - @Test - public void return_health() { - healthChecker.setHealth(Health.newHealthCheckBuilder() - .setStatus(Health.Status.YELLOW) - .addCause("foo") - .addCause("bar") - .build()); - - ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); - assertThatAttributeIs(protobuf, "Health", "YELLOW"); - SystemInfoTesting.assertThatAttributeHasOnlyValues(protobuf, "Health Causes", asList("foo", "bar")); - } -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SystemSectionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SystemSectionTest.java index 94378667a59..c1ae9b3305d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SystemSectionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SystemSectionTest.java @@ -19,26 +19,199 @@ */ package org.sonar.server.platform.monitoring; +import java.io.File; +import java.util.Optional; +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.platform.Server; +import org.sonar.api.security.SecurityRealm; +import org.sonar.api.utils.log.LoggerLevel; import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.authentication.IdentityProviderRepositoryRule; +import org.sonar.server.authentication.TestIdentityProvider; +import org.sonar.server.health.Health; +import org.sonar.server.health.TestStandaloneHealthChecker; +import org.sonar.server.platform.ServerId; +import org.sonar.server.platform.ServerIdLoader; +import org.sonar.server.platform.ServerLogging; +import org.sonar.server.user.SecurityRealmFactory; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.sonar.process.systeminfo.SystemInfoUtils.attribute; +import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs; public class SystemSectionTest { - private SystemSection underTest = new SystemSection(); + private static final String SERVER_ID_PROPERTY = "Server ID"; + private static final String SERVER_ID_VALIDATED_PROPERTY = "Server ID validated"; + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public IdentityProviderRepositoryRule identityProviderRepository = new IdentityProviderRepositoryRule(); + + private MapSettings settings = new MapSettings(); + private Server server = mock(Server.class); + private ServerIdLoader serverIdLoader = mock(ServerIdLoader.class); + private ServerLogging serverLogging = mock(ServerLogging.class); + private SecurityRealmFactory securityRealmFactory = mock(SecurityRealmFactory.class); + private TestStandaloneHealthChecker healthChecker = new TestStandaloneHealthChecker(); + + private SystemSection underTest = new SystemSection(settings.asConfig(), securityRealmFactory, identityProviderRepository, server, + serverLogging, serverIdLoader, healthChecker); + + @Before + public void setUp() throws Exception { + when(serverLogging.getRootLoggerLevel()).thenReturn(LoggerLevel.DEBUG); + when(serverIdLoader.getRaw()).thenReturn(Optional.empty()); + when(serverIdLoader.get()).thenReturn(Optional.empty()); + } + + @Test + public void name_is_not_empty() { + assertThat(underTest.name()).isNotEmpty(); + } + + @Test + public void test_getServerId() { + when(serverIdLoader.getRaw()).thenReturn(Optional.of("ABC")); + assertThat(underTest.getServerId()).isEqualTo("ABC"); + + when(serverIdLoader.getRaw()).thenReturn(Optional.empty()); + assertThat(underTest.getServerId()).isNull(); + } + + @Test + public void attributes_contain_information_about_valid_server_id() { + when(serverIdLoader.get()).thenReturn(Optional.of(new ServerId("ABC", true))); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, SERVER_ID_PROPERTY, "ABC"); + assertThatAttributeIs(protobuf, SERVER_ID_VALIDATED_PROPERTY, true); + } + + @Test + public void attributes_contain_information_about_non_valid_server_id() { + when(serverIdLoader.get()).thenReturn(Optional.of(new ServerId("ABC", false))); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, SERVER_ID_PROPERTY, "ABC"); + assertThatAttributeIs(protobuf, SERVER_ID_VALIDATED_PROPERTY, false); + } + + @Test + public void attributes_do_not_contain_information_about_server_id_if_absent() { + when(serverIdLoader.get()).thenReturn(Optional.empty()); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThat(attribute(protobuf, SERVER_ID_PROPERTY)).isNull(); + assertThat(attribute(protobuf, SERVER_ID_VALIDATED_PROPERTY)).isNull(); + } @Test - public void name() { - assertThat(underTest.toProtobuf().getName()).isEqualTo("System"); + public void official_distribution() throws Exception { + File rootDir = temp.newFolder(); + FileUtils.write(new File(rootDir, SystemSection.BRANDING_FILE_PATH), "1.2"); + + when(server.getRootDir()).thenReturn(rootDir); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "Official Distribution", true); + } + + @Test + public void not_an_official_distribution() throws Exception { + File rootDir = temp.newFolder(); + // branding file is missing + when(server.getRootDir()).thenReturn(rootDir); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "Official Distribution", false); + } + + @Test + public void get_log_level() throws Exception { + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "Logs Level", "DEBUG"); + } + + @Test + public void get_realm() throws Exception { + SecurityRealm realm = mock(SecurityRealm.class); + when(realm.getName()).thenReturn("LDAP"); + when(securityRealmFactory.getRealm()).thenReturn(realm); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "External User Authentication", "LDAP"); + } + + @Test + public void no_realm() throws Exception { + when(securityRealmFactory.getRealm()).thenReturn(null); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThat(attribute(protobuf, "External User Authentication")).isNull(); + } + + @Test + public void get_enabled_identity_providers() throws Exception { + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("github") + .setName("GitHub") + .setEnabled(true)); + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("bitbucket") + .setName("Bitbucket") + .setEnabled(true)); + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("disabled") + .setName("Disabled") + .setEnabled(false)); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "Accepted external identity providers", "Bitbucket, GitHub"); + } + + @Test + public void get_enabled_identity_providers_allowing_users_to_signup() throws Exception { + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("github") + .setName("GitHub") + .setEnabled(true) + .setAllowsUsersToSignUp(true)); + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("bitbucket") + .setName("Bitbucket") + .setEnabled(true) + .setAllowsUsersToSignUp(false)); + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("disabled") + .setName("Disabled") + .setEnabled(false) + .setAllowsUsersToSignUp(true)); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "External identity providers whose users are allowed to sign themselves up", "GitHub"); } @Test - public void system_properties() { - ProtobufSystemInfo.Section section = underTest.toProtobuf(); + public void return_health() { + healthChecker.setHealth(Health.newHealthCheckBuilder() + .setStatus(Health.Status.YELLOW) + .addCause("foo") + .addCause("bar") + .build()); - assertThat(attribute(section, "System Date").getStringValue()).isNotEmpty(); - assertThat(attribute(section, "Processors").getLongValue()).isGreaterThan(0); + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "Health", "YELLOW"); + SystemInfoTesting.assertThatAttributeHasOnlyValues(protobuf, "Health Causes", asList("foo", "bar")); } } diff --git a/server/sonar-web/src/main/js/api/system.ts b/server/sonar-web/src/main/js/api/system.ts index 1b4fd125a97..df9da694e40 100644 --- a/server/sonar-web/src/main/js/api/system.ts +++ b/server/sonar-web/src/main/js/api/system.ts @@ -44,14 +44,13 @@ export interface NodeInfo extends SysValueObject { Name: string; Health: HealthType; 'Health Causes': HealthCause[]; + 'Logs Level': string; } export interface SysInfo extends SysValueObject { Cluster: boolean; Health: HealthType; 'Health Causes': HealthCause[]; - 'Application Nodes': NodeInfo[]; - 'Search Nodes': NodeInfo[]; } export function setLogLevel(level: string): Promise<void | Response> { diff --git a/server/sonar-web/src/main/js/apps/system/__tests__/system-test.js b/server/sonar-web/src/main/js/apps/system/__tests__/system-test.js deleted file mode 100644 index 0ad07b2794a..00000000000 --- a/server/sonar-web/src/main/js/apps/system/__tests__/system-test.js +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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. - */ -import React from 'react'; -import { shallow } from 'enzyme'; -import ItemValue from '../item-value'; -import ItemBoolean from '../item-boolean'; -import ItemObject from '../item-object'; -import ItemLogLevel from '../item-log-level'; - -describe('Item Value', () => { - it('should render string', () => { - const result = shallow(<ItemValue value="/some/path/as/an/example" />); - expect(result.find('code').text()).toBe('/some/path/as/an/example'); - }); -}); - -describe('ItemBoolean', () => { - it('should render `true`', () => { - const result = shallow(<ItemBoolean value={true} />); - expect(result.find('.icon-check').length).toBe(1); - }); - - it('should render `false`', () => { - const result = shallow(<ItemBoolean value={false} />); - expect(result.find('.icon-delete').length).toBe(1); - }); -}); - -describe('ItemObject', () => { - it('should render object', () => { - const result = shallow(<ItemObject value={{ name: 'Java', version: '3.2' }} />); - expect(result.find('table').length).toBe(1); - expect(result.find('tr').length).toBe(2); - }); - - it('should render `true` inside object', () => { - const result = shallow(<ItemObject value={{ isCool: true }} />); - const itemValue = result.find(ItemValue); - expect(itemValue.length).toBe(1); - expect(itemValue.prop('value')).toBe(true); - }); - - it('should render object inside object', () => { - const result = shallow( - <ItemObject value={{ users: { docs: 1, shards: 5 }, tests: { docs: 68, shards: 5 } }} /> - ); - expect(result.find(ItemValue).length).toBe(2); - expect( - result - .find(ItemValue) - .at(0) - .prop('value') - ).toEqual({ docs: 1, shards: 5 }); - expect( - result - .find(ItemValue) - .at(1) - .prop('value') - ).toEqual({ docs: 68, shards: 5 }); - }); -}); - -describe('Log Level', () => { - it('should render select box', () => { - const result = shallow(<ItemLogLevel value="INFO" />); - expect(result.find('select').length).toBe(1); - expect(result.find('option').length).toBe(3); - }); - - it('should set initial value', () => { - const result = shallow(<ItemLogLevel value="DEBUG" />); - expect(result.find('select').prop('value')).toBe('DEBUG'); - }); - - it('should render warning', () => { - const result = shallow(<ItemLogLevel value="DEBUG" />); - expect(result.find('.alert').length).toBe(1); - }); - - it('should not render warning', () => { - const result = shallow(<ItemLogLevel value="INFO" />); - expect(result.find('.alert').length).toBe(0); - }); -}); diff --git a/server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts index cd10c605418..0c550fd943f 100644 --- a/server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts @@ -17,27 +17,43 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import * as utils from '../utils'; +import * as u from '../utils'; describe('parseQuery', () => { it('should correctly parse the expand array', () => { - expect(utils.parseQuery({})).toEqual({ expandedCards: [] }); - expect(utils.parseQuery({ expand: 'foo,bar' })).toEqual({ expandedCards: ['foo', 'bar'] }); + expect(u.parseQuery({})).toEqual({ expandedCards: [] }); + expect(u.parseQuery({ expand: 'foo,bar' })).toEqual({ expandedCards: ['foo', 'bar'] }); }); }); describe('serializeQuery', () => { it('should correctly serialize the expand array', () => { - expect(utils.serializeQuery({ expandedCards: [] })).toEqual({}); - expect(utils.serializeQuery({ expandedCards: ['foo', 'bar'] })).toEqual({ expand: 'foo,bar' }); + expect(u.serializeQuery({ expandedCards: [] })).toEqual({}); + expect(u.serializeQuery({ expandedCards: ['foo', 'bar'] })).toEqual({ expand: 'foo,bar' }); }); }); describe('groupSections', () => { it('should correctly group the root field into a main section', () => { - expect(utils.groupSections({ foo: 'Foo', bar: 3, baz: { a: 'a' } })).toEqual({ + expect(u.groupSections({ foo: 'Foo', bar: 3, baz: { a: 'a' } })).toEqual({ mainSection: { foo: 'Foo', bar: 3 }, sections: { baz: { a: 'a' } } }); }); }); + +describe('getSystemLogsLevel', () => { + it('should correctly return log level for standalone mode', () => { + expect(u.getSystemLogsLevel({ 'Logs Level': 'FOO' } as u.StandaloneSysInfo)).toBe('FOO'); + expect(u.getSystemLogsLevel({} as u.StandaloneSysInfo)).toBe('INFO'); + expect(u.getSystemLogsLevel()).toBe('INFO'); + }); + it('should return the worst log level for cluster mode', () => { + expect( + u.getSystemLogsLevel({ + Cluster: true, + 'Application Nodes': [{ 'Logs Level': 'INFO' }, { 'Logs Level': 'DEBUG' }] + } as u.ClusterSysInfo) + ).toBe('DEBUG'); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/system/components/App.tsx b/server/sonar-web/src/main/js/apps/system/components/App.tsx index 440b117929a..1cd3752de88 100644 --- a/server/sonar-web/src/main/js/apps/system/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/App.tsx @@ -22,10 +22,18 @@ import * as PropTypes from 'prop-types'; import Helmet from 'react-helmet'; import ClusterSysInfos from './ClusterSysInfos'; import PageHeader from './PageHeader'; -import StandAloneSysInfos from './StandAloneSysInfos'; +import StandaloneSysInfos from './StandaloneSysInfos'; import { translate } from '../../../helpers/l10n'; import { getSystemInfo, SysInfo } from '../../../api/system'; -import { isCluster, parseQuery, Query, serializeQuery } from '../utils'; +import { + ClusterSysInfo, + getSystemLogsLevel, + isCluster, + parseQuery, + Query, + serializeQuery, + StandaloneSysInfo +} from '../utils'; import { RawQuery } from '../../../helpers/query'; import '../styles.css'; @@ -84,7 +92,7 @@ export default class App extends React.PureComponent<Props, State> { updateQuery = (newQuery: Query) => { const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery }); - this.context.router.push({ pathname: this.props.location.pathname, query }); + this.context.router.replace({ pathname: this.props.location.pathname, query }); }; renderSysInfo() { @@ -97,26 +105,32 @@ export default class App extends React.PureComponent<Props, State> { if (isCluster(sysInfoData)) { return ( <ClusterSysInfos - sysInfoData={sysInfoData} expandedCards={query.expandedCards} + sysInfoData={sysInfoData as ClusterSysInfo} toggleCard={this.toggleSysInfoCards} /> ); } - return <StandAloneSysInfos sysInfoData={sysInfoData} />; + return ( + <StandaloneSysInfos + expandedCards={query.expandedCards} + sysInfoData={sysInfoData as StandaloneSysInfo} + toggleCard={this.toggleSysInfoCards} + /> + ); } render() { const { loading, sysInfoData } = this.state; - // TODO Correctly get logLevel, we are not sure yet how we want to do it for cluster mode return ( <div className="page page-limited"> <Helmet title={translate('system_info.page')} /> <PageHeader loading={loading} isCluster={isCluster(sysInfoData)} - logLevel="INFO" + logLevel={getSystemLogsLevel(sysInfoData)} showActions={sysInfoData != undefined} + onLogLevelChange={this.fetchSysInfo} /> {this.renderSysInfo()} </div> diff --git a/server/sonar-web/src/main/js/apps/system/components/ChangeLogLevelForm.tsx b/server/sonar-web/src/main/js/apps/system/components/ChangeLogLevelForm.tsx index 3065a0be66d..6118fe2d956 100644 --- a/server/sonar-web/src/main/js/apps/system/components/ChangeLogLevelForm.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/ChangeLogLevelForm.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import Modal from 'react-modal'; import { setLogLevel } from '../../../api/system'; import { translate } from '../../../helpers/l10n'; +import { LOGS_LEVELS } from '../utils'; interface Props { infoMsg: string; @@ -34,8 +35,6 @@ interface State { updating: boolean; } -const LOG_LEVELS = ['INFO', 'DEBUG', 'TRACE']; - export default class ChangeLogLevelForm extends React.PureComponent<Props, State> { constructor(props: Props) { super(props); @@ -78,9 +77,10 @@ export default class ChangeLogLevelForm extends React.PureComponent<Props, State <h2>{header}</h2> </div> <div className="modal-body"> - {LOG_LEVELS.map(level => ( + {LOGS_LEVELS.map(level => ( <p key={level} className="spacer-bottom"> <input + id={`loglevel-${level}`} type="radio" className="spacer-right text-middle" name="system.log_levels" @@ -88,7 +88,7 @@ export default class ChangeLogLevelForm extends React.PureComponent<Props, State checked={level === newLevel} onChange={this.handleLevelChange} /> - {level} + <label htmlFor={`loglevel-${level}`}>{level}</label> </p> ))} <div className="alert alert-info spacer-top">{this.props.infoMsg}</div> diff --git a/server/sonar-web/src/main/js/apps/system/components/ClusterSysInfos.tsx b/server/sonar-web/src/main/js/apps/system/components/ClusterSysInfos.tsx index 7f68dc6771a..cb3000dea32 100644 --- a/server/sonar-web/src/main/js/apps/system/components/ClusterSysInfos.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/ClusterSysInfos.tsx @@ -22,19 +22,19 @@ import { sortBy } from 'lodash'; import HealthCard from './info-items/HealthCard'; import { translate } from '../../../helpers/l10n'; import { + ClusterSysInfo, getAppNodes, getHealth, getHealthCauses, - getMainCardSection, + getClusterMainCardSection, getNodeName, getSearchNodes, ignoreInfoFields } from '../utils'; -import { SysInfo } from '../../../api/system'; interface Props { expandedCards: string[]; - sysInfoData: SysInfo; + sysInfoData: ClusterSysInfo; toggleCard: (toggledCard: string) => void; } @@ -49,7 +49,7 @@ export default function ClusterSysInfos({ expandedCards, sysInfoData, toggleCard name={mainCardName} onClick={toggleCard} open={expandedCards.includes(mainCardName)} - sysInfoData={ignoreInfoFields(getMainCardSection(sysInfoData))} + sysInfoData={ignoreInfoFields(getClusterMainCardSection(sysInfoData))} /> <li className="note system-info-health-title"> {translate('system.application_nodes_title')} diff --git a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx index 1b2ef92d34a..4e26c343f02 100644 --- a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx @@ -28,6 +28,7 @@ interface Props { canRestart: boolean; cluster: boolean; logLevel: string; + onLogLevelChange: () => void; } interface State { @@ -40,9 +41,9 @@ export default class PageActions extends React.PureComponent<Props, State> { constructor(props: Props) { super(props); this.state = { + logLevel: props.logLevel, openLogsLevelForm: false, - openRestartForm: false, - logLevel: props.logLevel + openRestartForm: false }; } @@ -59,6 +60,7 @@ export default class PageActions extends React.PureComponent<Props, State> { handleLogsLevelChange = (logLevel: string) => { this.setState({ logLevel }); + this.props.onLogLevelChange(); this.handleLogsLevelClose(); }; diff --git a/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx index c9d84569e30..97c4f1077c0 100644 --- a/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx @@ -26,21 +26,23 @@ interface Props { loading: boolean; logLevel: string; showActions: boolean; + onLogLevelChange: () => void; } -export default function PageHeader({ isCluster, loading, logLevel, showActions }: Props) { +export default function PageHeader(props: Props) { return ( <header className="page-header"> <h1 className="page-title">{translate('system_info.page')}</h1> - {showActions && ( + {props.showActions && ( <PageActions - canDownloadLogs={!isCluster} - canRestart={!isCluster} - cluster={isCluster} - logLevel={logLevel} + canDownloadLogs={!props.isCluster} + canRestart={!props.isCluster} + cluster={props.isCluster} + logLevel={props.logLevel} + onLogLevelChange={props.onLogLevelChange} /> )} - {loading && ( + {props.loading && ( <div className="page-actions"> <i className="spinner" /> </div> diff --git a/server/sonar-web/src/main/js/apps/system/components/StandaloneSysInfos.tsx b/server/sonar-web/src/main/js/apps/system/components/StandaloneSysInfos.tsx new file mode 100644 index 00000000000..4ed444cc66d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/system/components/StandaloneSysInfos.tsx @@ -0,0 +1,62 @@ +/* + * 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. + */ +import * as React from 'react'; +import { map } from 'lodash'; +import HealthCard from './info-items/HealthCard'; +import { + getHealth, + getHealthCauses, + getStandaloneMainSections, + getStandaloneSecondarySections, + ignoreInfoFields, + StandaloneSysInfo +} from '../utils'; + +interface Props { + expandedCards: string[]; + sysInfoData: StandaloneSysInfo; + toggleCard: (toggledCard: string) => void; +} + +export default function StandAloneSysInfos({ expandedCards, sysInfoData, toggleCard }: Props) { + const mainCardName = 'System'; + return ( + <ul> + <HealthCard + biggerHealth={true} + health={getHealth(sysInfoData)} + healthCauses={getHealthCauses(sysInfoData)} + name={mainCardName} + onClick={toggleCard} + open={expandedCards.includes(mainCardName)} + sysInfoData={ignoreInfoFields(getStandaloneMainSections(sysInfoData))} + /> + {map(getStandaloneSecondarySections(sysInfoData), (section, name) => ( + <HealthCard + key={name} + name={name} + onClick={toggleCard} + open={expandedCards.includes(name)} + sysInfoData={ignoreInfoFields(section)} + /> + ))} + </ul> + ); +} diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/ClusterSysInfos-test.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/ClusterSysInfos-test.tsx index 5b703a1a214..21c60cbd6fc 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/ClusterSysInfos-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/ClusterSysInfos-test.tsx @@ -20,18 +20,38 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import ClusterSysInfos from '../ClusterSysInfos'; -import { HealthType, SysInfo } from '../../../../api/system'; +import { HealthType } from '../../../../api/system'; +import { ClusterSysInfo } from '../../utils'; -const sysInfoData: SysInfo = { +const sysInfoData: ClusterSysInfo = { Cluster: true, Health: HealthType.RED, Name: 'Foo', 'Health Causes': [{ message: 'Database down' }], - 'Application Nodes': [{ Name: 'Bar', Health: HealthType.GREEN, 'Health Causes': [] }], - 'Search Nodes': [{ Name: 'Baz', Health: HealthType.YELLOW, 'Health Causes': [] }] + 'Application Nodes': [ + { Name: 'Bar', Health: HealthType.GREEN, 'Health Causes': [], 'Logs Level': 'INFO' } + ], + 'Search Nodes': [ + { Name: 'Baz', Health: HealthType.YELLOW, 'Health Causes': [], 'Logs Level': 'INFO' } + ] }; it('should render correctly', () => { + expect( + getWrapper({ + sysInfoData: { + ...sysInfoData, + 'Application Nodes': [ + { Name: 'Foo', Health: HealthType.GREEN, 'Health Causes': [], 'Logs Level': 'INFO' }, + { Name: 'Bar', Health: HealthType.RED, 'Health Causes': [], 'Logs Level': 'DEBUG' }, + { Name: 'Baz', Health: HealthType.YELLOW, 'Health Causes': [], 'Logs Level': 'TRACE' } + ] + } + }).find('HealthCard') + ).toHaveLength(5); +}); + +it('should support more than two nodes', () => { expect(getWrapper()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx index 098e897ec92..627347f8886 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx @@ -25,7 +25,13 @@ import { click } from '../../../../helpers/testUtils'; it('should render correctly', () => { expect( shallow( - <PageActions canDownloadLogs={true} canRestart={true} cluster={false} logLevel="INFO" /> + <PageActions + canDownloadLogs={true} + canRestart={true} + cluster={false} + logLevel="INFO" + onLogLevelChange={() => {}} + /> ) ).toMatchSnapshot(); }); @@ -33,14 +39,26 @@ it('should render correctly', () => { it('should render without restart and log download', () => { expect( shallow( - <PageActions canDownloadLogs={false} canRestart={false} cluster={true} logLevel="INFO" /> + <PageActions + canDownloadLogs={false} + canRestart={false} + cluster={true} + logLevel="INFO" + onLogLevelChange={() => {}} + /> ) ).toMatchSnapshot(); }); it('should open restart modal', () => { const wrapper = shallow( - <PageActions canDownloadLogs={true} canRestart={true} cluster={false} logLevel="INFO" /> + <PageActions + canDownloadLogs={true} + canRestart={true} + cluster={false} + logLevel="INFO" + onLogLevelChange={() => {}} + /> ); click(wrapper.find('#restart-server-button')); expect(wrapper.find('RestartForm')).toHaveLength(1); @@ -48,7 +66,13 @@ it('should open restart modal', () => { it('should open change log level modal', () => { const wrapper = shallow( - <PageActions canDownloadLogs={true} canRestart={true} cluster={false} logLevel="INFO" /> + <PageActions + canDownloadLogs={true} + canRestart={true} + cluster={false} + logLevel="INFO" + onLogLevelChange={() => {}} + /> ); click(wrapper.find('#edit-logs-level-button')); expect(wrapper.find('ChangeLogLevelForm')).toHaveLength(1); diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx index f5255c7228e..a101f213406 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx @@ -23,12 +23,28 @@ import PageHeader from '../PageHeader'; it('should render correctly', () => { expect( - shallow(<PageHeader isCluster={true} loading={false} logLevel="INFO" showActions={true} />) + shallow( + <PageHeader + isCluster={true} + loading={false} + logLevel="INFO" + showActions={true} + onLogLevelChange={() => {}} + /> + ) ).toMatchSnapshot(); }); it('should show a loading spinner and no actions', () => { expect( - shallow(<PageHeader isCluster={true} loading={true} logLevel="INFO" showActions={false} />) + shallow( + <PageHeader + isCluster={true} + loading={true} + logLevel="INFO" + showActions={false} + onLogLevelChange={() => {}} + /> + ) ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/system/item-value.js b/server/sonar-web/src/main/js/apps/system/components/__tests__/StandaloneSysInfos-test.tsx index 5461aa95e8b..c70202e3219 100644 --- a/server/sonar-web/src/main/js/apps/system/item-value.js +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/StandaloneSysInfos-test.tsx @@ -17,29 +17,34 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; -import ItemBoolean from './item-boolean'; -import ItemObject from './item-object'; -import ItemLogLevel from './item-log-level'; +import * as React from 'react'; +import { shallow } from 'enzyme'; +import StandaloneSysInfos from '../StandaloneSysInfos'; +import { HealthType } from '../../../../api/system'; +import { StandaloneSysInfo } from '../../utils'; -export default class ItemValue extends React.PureComponent { - render() { - if (this.props.name === 'Logs Level') { - return <ItemLogLevel value={this.props.value} />; - } +const sysInfoData: StandaloneSysInfo = { + Cluster: true, + Health: HealthType.RED, + 'Logs Level': 'DEBUG', + Name: 'Foo', + 'Health Causes': [{ message: 'Database down' }], + 'Web JVM': { 'Max Memory': '2Gb' }, + 'Compute Engine': { Pending: 4 }, + Elasticsearch: { 'Number of Nodes': 1 } +}; - const rawValue = this.props.value; - let formattedValue; - switch (typeof this.props.value) { - case 'boolean': - formattedValue = <ItemBoolean value={rawValue} />; - break; - case 'object': - formattedValue = <ItemObject value={rawValue} />; - break; - default: - formattedValue = <code>{rawValue}</code>; - } - return formattedValue; - } +it('should render correctly', () => { + expect(getWrapper()).toMatchSnapshot(); +}); + +function getWrapper(props = {}) { + return shallow( + <StandaloneSysInfos + expandedCards={['Compute Engine', 'Foo']} + sysInfoData={sysInfoData} + toggleCard={() => {}} + {...props} + /> + ); } diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ChangeLogLevelForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ChangeLogLevelForm-test.tsx.snap index 04cfc11d49b..eeaa775462d 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ChangeLogLevelForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ChangeLogLevelForm-test.tsx.snap @@ -34,12 +34,17 @@ exports[`should display some warning messages for non INFO levels 1`] = ` <input checked={false} className="spacer-right text-middle" + id="loglevel-INFO" name="system.log_levels" onChange={[Function]} type="radio" value="INFO" /> - INFO + <label + htmlFor="loglevel-INFO" + > + INFO + </label> </p> <p className="spacer-bottom" @@ -47,12 +52,17 @@ exports[`should display some warning messages for non INFO levels 1`] = ` <input checked={true} className="spacer-right text-middle" + id="loglevel-DEBUG" name="system.log_levels" onChange={[Function]} type="radio" value="DEBUG" /> - DEBUG + <label + htmlFor="loglevel-DEBUG" + > + DEBUG + </label> </p> <p className="spacer-bottom" @@ -60,12 +70,17 @@ exports[`should display some warning messages for non INFO levels 1`] = ` <input checked={false} className="spacer-right text-middle" + id="loglevel-TRACE" name="system.log_levels" onChange={[Function]} type="radio" value="TRACE" /> - TRACE + <label + htmlFor="loglevel-TRACE" + > + TRACE + </label> </p> <div className="alert alert-info spacer-top" @@ -133,12 +148,17 @@ exports[`should render correctly 1`] = ` <input checked={true} className="spacer-right text-middle" + id="loglevel-INFO" name="system.log_levels" onChange={[Function]} type="radio" value="INFO" /> - INFO + <label + htmlFor="loglevel-INFO" + > + INFO + </label> </p> <p className="spacer-bottom" @@ -146,12 +166,17 @@ exports[`should render correctly 1`] = ` <input checked={false} className="spacer-right text-middle" + id="loglevel-DEBUG" name="system.log_levels" onChange={[Function]} type="radio" value="DEBUG" /> - DEBUG + <label + htmlFor="loglevel-DEBUG" + > + DEBUG + </label> </p> <p className="spacer-bottom" @@ -159,12 +184,17 @@ exports[`should render correctly 1`] = ` <input checked={false} className="spacer-right text-middle" + id="loglevel-TRACE" name="system.log_levels" onChange={[Function]} type="radio" value="TRACE" /> - TRACE + <label + htmlFor="loglevel-TRACE" + > + TRACE + </label> </p> <div className="alert alert-info spacer-top" diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ClusterSysInfos-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ClusterSysInfos-test.tsx.snap index 20ed8d541f6..abd7d3a63ac 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ClusterSysInfos-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ClusterSysInfos-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly 1`] = ` +exports[`should support more than two nodes 1`] = ` <ul> <HealthCard biggerHealth={true} @@ -34,6 +34,7 @@ exports[`should render correctly 1`] = ` open={false} sysInfoData={ Object { + "Logs Level": "INFO", "Name": "Bar", } } @@ -51,6 +52,7 @@ exports[`should render correctly 1`] = ` open={false} sysInfoData={ Object { + "Logs Level": "INFO", "Name": "Baz", } } diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageHeader-test.tsx.snap index 2945b6df932..39f93da4322 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageHeader-test.tsx.snap @@ -14,6 +14,7 @@ exports[`should render correctly 1`] = ` canRestart={false} cluster={true} logLevel="INFO" + onLogLevelChange={[Function]} /> </header> `; diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/StandaloneSysInfos-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/StandaloneSysInfos-test.tsx.snap new file mode 100644 index 00000000000..f4961aeac1e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/StandaloneSysInfos-test.tsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<ul> + <HealthCard + biggerHealth={true} + health="RED" + healthCauses={ + Array [ + Object { + "message": "Database down", + }, + ] + } + name="System" + onClick={[Function]} + open={false} + sysInfoData={ + Object { + "Logs Level": "DEBUG", + "Name": "Foo", + } + } + /> + <HealthCard + name="Web" + onClick={[Function]} + open={false} + sysInfoData={ + Object { + "Web Database Connectivity": undefined, + "Web JVM": Object { + "Max Memory": "2Gb", + }, + "Web JVM Properties": undefined, + } + } + /> + <HealthCard + name="Compute Engine" + onClick={[Function]} + open={true} + sysInfoData={ + Object { + "Compute Engine JVM": undefined, + "Compute Engine JVM Properties": undefined, + "Pending": 4, + } + } + /> + <HealthCard + name="Search" + onClick={[Function]} + open={false} + sysInfoData={ + Object { + "Elasticsearch": Object { + "Number of Nodes": 1, + }, + "Search JVM": undefined, + "Search JVM Properties": undefined, + } + } + /> +</ul> +`; diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx b/server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx index b45205dccba..459f26ee721 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx @@ -24,12 +24,13 @@ import HealthItem from './HealthItem'; import OpenCloseIcon from '../../../../components/icons-components/OpenCloseIcon'; import Section from './Section'; import { HealthType, HealthCause, SysValueObject } from '../../../../api/system'; -import { groupSections } from '../../utils'; +import { LOGS_LEVELS, groupSections, getLogsLevel } from '../../utils'; +import { translate } from '../../../../helpers/l10n'; interface Props { biggerHealth?: boolean; - health: HealthType; - healthCauses: HealthCause[]; + health?: HealthType; + healthCauses?: HealthCause[]; onClick: (toggledCard: string) => void; open: boolean; name: string; @@ -48,10 +49,12 @@ export default class HealthCard extends React.PureComponent<Props, State> { onDetailLeave = () => this.setState({ hoveringDetail: false }); render() { - const { open, sysInfoData } = this.props; + const { health, open, sysInfoData } = this.props; const { mainSection, sections } = groupSections(sysInfoData); const showFields = open && mainSection && Object.keys(mainSection).length > 0; const showSections = open && sections; + const logLevel = getLogsLevel(sysInfoData); + const showLogLevelWarning = logLevel && logLevel !== LOGS_LEVELS[0]; return ( <li className={classNames('boxed-group system-info-health-card', { @@ -62,11 +65,19 @@ export default class HealthCard extends React.PureComponent<Props, State> { <OpenCloseIcon className="little-spacer-right" open={open} /> {this.props.name} </span> - <HealthItem - className={classNames('pull-right', { 'big-dot': this.props.biggerHealth })} - health={this.props.health} - healthCauses={this.props.healthCauses} - /> + {health && ( + <HealthItem + biggerHealth={this.props.biggerHealth} + className="pull-right spacer-left" + health={health} + healthCauses={this.props.healthCauses} + /> + )} + {showLogLevelWarning && ( + <span className="pull-right alert alert-danger"> + {translate('system.log_level.warning.short')} + </span> + )} </div> {open && ( <div diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/HealthItem.tsx b/server/sonar-web/src/main/js/apps/system/components/info-items/HealthItem.tsx index a0793a81831..86618c34c8a 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/HealthItem.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/HealthItem.tsx @@ -20,15 +20,17 @@ import * as React from 'react'; import * as classNames from 'classnames'; import HealthCauseItem from './HealthCauseItem'; +import StatusIndicator from '../../../../components/common/StatusIndicator'; import { HealthType, HealthCause } from '../../../../api/system'; interface Props { + biggerHealth?: boolean; className?: string; health: HealthType; healthCauses?: HealthCause[]; } -export default function HealthItem({ className, health, healthCauses }: Props) { +export default function HealthItem({ biggerHealth, className, health, healthCauses }: Props) { const hasHealthCauses = healthCauses && healthCauses.length > 0 && health !== HealthType.GREEN; return ( <div className={classNames('system-info-health-info', className)}> @@ -36,7 +38,7 @@ export default function HealthItem({ className, health, healthCauses }: Props) { healthCauses!.map((cause, idx) => ( <HealthCauseItem key={idx} className="spacer-right" health={health} healthCause={cause} /> ))} - <span className={classNames('system-info-health-dot', health)} /> + <StatusIndicator color={health.toLowerCase()} size={biggerHealth ? 'big' : undefined} /> </div> ); } diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthCard-test.tsx b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthCard-test.tsx index c080f444dec..98b33bd31ee 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthCard-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthCard-test.tsx @@ -48,6 +48,12 @@ it('should show a main section and multiple sub sections', () => { expect(getShallowWrapper({ open: true, sysInfoData })).toMatchSnapshot(); }); +it('should display the log level alert', () => { + expect( + getShallowWrapper({ sysInfoData: { 'Logs Level': 'DEBUG' } }).find('.alert') + ).toMatchSnapshot(); +}); + function getShallowWrapper(props = {}) { return shallow( <HealthCard diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthItem-test.tsx b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthItem-test.tsx index f77622b9e92..9f1bd198352 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthItem-test.tsx @@ -24,7 +24,9 @@ import { HealthType } from '../../../../../api/system'; it('should render correctly', () => { expect( - shallow(<HealthItem health={HealthType.RED} healthCauses={[{ message: 'foo' }]} />) + shallow( + <HealthItem biggerHealth={true} health={HealthType.RED} healthCauses={[{ message: 'foo' }]} /> + ) ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthCard-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthCard-test.tsx.snap index 724001990fa..7390ef8020e 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthCard-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthCard-test.tsx.snap @@ -1,5 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should display the log level alert 1`] = ` +<span + className="pull-right alert alert-danger" +> + system.log_level.warning.short +</span> +`; + exports[`should display the sysinfo detail 1`] = ` <li className="boxed-group system-info-health-card" @@ -18,7 +26,8 @@ exports[`should display the sysinfo detail 1`] = ` Foobar </span> <HealthItem - className="pull-right big-dot" + biggerHealth={true} + className="pull-right spacer-left" health="RED" healthCauses={ Array [ @@ -55,7 +64,8 @@ exports[`should render correctly 1`] = ` Foobar </span> <HealthItem - className="pull-right" + biggerHealth={false} + className="pull-right spacer-left" health="RED" healthCauses={ Array [ @@ -87,7 +97,8 @@ exports[`should show a main section and multiple sub sections 1`] = ` Foobar </span> <HealthItem - className="pull-right" + biggerHealth={false} + className="pull-right spacer-left" health="RED" healthCauses={ Array [ diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthItem-test.tsx.snap index d9fa53a2956..f962437404c 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthItem-test.tsx.snap @@ -4,8 +4,8 @@ exports[`should not render health causes 1`] = ` <div className="system-info-health-info" > - <span - className="system-info-health-dot GREEN" + <StatusIndicator + color="green" /> </div> `; @@ -14,8 +14,8 @@ exports[`should not render health causes 2`] = ` <div className="system-info-health-info" > - <span - className="system-info-health-dot YELLOW" + <StatusIndicator + color="yellow" /> </div> `; @@ -33,8 +33,9 @@ exports[`should render correctly 1`] = ` } } /> - <span - className="system-info-health-dot RED" + <StatusIndicator + color="red" + size="big" /> </div> `; @@ -61,8 +62,8 @@ exports[`should render multiple health causes 1`] = ` } } /> - <span - className="system-info-health-dot YELLOW" + <StatusIndicator + color="yellow" /> </div> `; diff --git a/server/sonar-web/src/main/js/apps/system/item-boolean.js b/server/sonar-web/src/main/js/apps/system/item-boolean.js deleted file mode 100644 index aeac6e88390..00000000000 --- a/server/sonar-web/src/main/js/apps/system/item-boolean.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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. - */ -import React from 'react'; - -export default class ItemBoolean extends React.PureComponent { - render() { - if (this.props.value) { - return <i className="icon-check" />; - } else { - return <i className="icon-delete" />; - } - } -} diff --git a/server/sonar-web/src/main/js/apps/system/item-log-level.js b/server/sonar-web/src/main/js/apps/system/item-log-level.js deleted file mode 100644 index 89364f164d8..00000000000 --- a/server/sonar-web/src/main/js/apps/system/item-log-level.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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. - */ -import React from 'react'; -import { setLogLevel } from '../../api/system'; -import { translate } from '../../helpers/l10n'; - -const LOG_LEVELS = ['INFO', 'DEBUG', 'TRACE']; - -export default class ItemLogLevel extends React.PureComponent { - constructor(props) { - super(props); - this.state = { level: props.value }; - } - - onChange = () => { - const newValue = this.refs.select.value; - setLogLevel(newValue).then(() => { - this.setState({ level: newValue }); - }); - }; - - render() { - const options = LOG_LEVELS.map(level => ( - <option key={level} value={level}> - {level} - </option> - )); - const warning = - this.state.level !== 'INFO' ? ( - <div className="alert alert-danger spacer-top" style={{ wordBreak: 'normal' }}> - {translate('system.log_level.warning')} - </div> - ) : null; - return ( - <div> - <select ref="select" onChange={this.onChange} value={this.state.level}> - {options} - </select> - {warning} - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/system/item-object.js b/server/sonar-web/src/main/js/apps/system/item-object.js deleted file mode 100644 index d9386cbce8d..00000000000 --- a/server/sonar-web/src/main/js/apps/system/item-object.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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. - */ -import React from 'react'; -import ItemValue from './item-value'; - -export default class ItemObject extends React.PureComponent { - render() { - const rows = Object.keys(this.props.value).map(key => { - return ( - <tr key={key}> - <td className="thin nowrap">{key}</td> - <td> - <ItemValue value={this.props.value[key]} /> - </td> - </tr> - ); - }); - return ( - <table className="data"> - <tbody>{rows}</tbody> - </table> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/system/main.js b/server/sonar-web/src/main/js/apps/system/main.js deleted file mode 100644 index 150bd042adf..00000000000 --- a/server/sonar-web/src/main/js/apps/system/main.js +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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. - */ -import React from 'react'; -import Helmet from 'react-helmet'; -import { sortBy } from 'lodash'; -import { getSystemInfo } from '../../api/system'; -import Section from './section'; -import { translate } from '../../helpers/l10n'; -import RestartForm from '../../components/common/RestartForm'; - -const SECTIONS_ORDER = [ - 'SonarQube', - 'Database', - 'System', - 'Elasticsearch State', - 'Elasticsearch', - 'Compute Engine Tasks', - 'Compute Engine State', - 'Compute Engine Database Connection', - 'JvmProperties' -]; - -export default class Main extends React.PureComponent { - state = { openRestartForm: false }; - - componentDidMount() { - getSystemInfo().then(info => this.setState({ sections: this.parseSections(info) })); - } - - parseSections = data => { - const sections = Object.keys(data).map(section => { - return { name: section, items: this.parseItems(data[section]) }; - }); - return this.orderSections(sections); - }; - - orderSections = sections => sortBy(sections, section => SECTIONS_ORDER.indexOf(section.name)); - - parseItems = data => { - const items = Object.keys(data).map(item => { - return { name: item, value: data[item] }; - }); - return this.orderItems(items); - }; - - orderItems = items => sortBy(items, 'name'); - - handleServerRestartOpen = () => this.setState({ openRestartForm: true }); - handleServerRestartClose = () => this.setState({ openRestartForm: false }); - - render() { - let sections = null; - if (this.state && this.state.sections) { - sections = this.state.sections - .filter(section => SECTIONS_ORDER.indexOf(section.name) >= 0) - .map(section => ( - <Section key={section.name} section={section.name} items={section.items} /> - )); - } - - return ( - <div className="page page-limited"> - <Helmet title={translate('system_info.page')} /> - <header className="page-header"> - <h1 className="page-title">{translate('system_info.page')}</h1> - <div className="page-actions"> - <a href={window.baseUrl + '/api/system/info'} id="download-link"> - Download - </a> - <div className="display-inline-block dropdown big-spacer-left"> - <button data-toggle="dropdown"> - Logs <i className="icon-dropdown" /> - </button> - <ul className="dropdown-menu"> - <li> - <a href={window.baseUrl + '/api/system/logs?process=app'} id="logs-link"> - Main Process - </a> - </li> - <li> - <a href={window.baseUrl + '/api/system/logs?process=ce'} id="ce-logs-link"> - Compute Engine - </a> - </li> - <li> - <a href={window.baseUrl + '/api/system/logs?process=es'} id="es-logs-link"> - Elasticsearch - </a> - </li> - <li> - <a href={window.baseUrl + '/api/system/logs?process=web'} id="web-logs-link"> - Web Server - </a> - </li> - </ul> - </div> - <button - id="restart-server-button" - className="big-spacer-left" - onClick={this.handleServerRestartOpen}> - Restart Server - </button> - {this.state.openRestartForm && <RestartForm onClose={this.handleServerRestartClose} />} - </div> - </header> - {sections} - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/system/routes.ts b/server/sonar-web/src/main/js/apps/system/routes.ts index 1a8d6a5ab38..fcffeff191c 100644 --- a/server/sonar-web/src/main/js/apps/system/routes.ts +++ b/server/sonar-web/src/main/js/apps/system/routes.ts @@ -17,19 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { RouterState, RouteComponent, IndexRouteProps } from 'react-router'; +import { RouterState, IndexRouteProps } from 'react-router'; const routes = [ { getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { import('./components/App').then(i => callback(null, { component: i.default })); } - }, - { - path: 'old', - getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) { - import('./main').then(i => callback(null, (i as any).default)); - } } ]; diff --git a/server/sonar-web/src/main/js/apps/system/section.js b/server/sonar-web/src/main/js/apps/system/section.js deleted file mode 100644 index 89b90e99c5d..00000000000 --- a/server/sonar-web/src/main/js/apps/system/section.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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. - */ -import React from 'react'; -import ItemValue from './item-value'; - -export default class Section extends React.PureComponent { - render() { - const items = this.props.items.map(item => { - return ( - <tr key={item.name}> - <td className="thin"> - <div style={{ width: '25vw', overflow: 'hidden', textOverflow: 'ellipsis' }}> - {item.name} - </div> - </td> - <td style={{ wordBreak: 'break-all' }}> - <ItemValue name={item.name} value={item.value} /> - </td> - </tr> - ); - }); - - return ( - <div className="big-spacer-bottom"> - <h3 className="spacer-bottom">{this.props.section}</h3> - <table className="data zebra" id={this.props.section}> - <tbody>{items}</tbody> - </table> - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/system/styles.css b/server/sonar-web/src/main/js/apps/system/styles.css index f1156609add..f34dc3fa394 100644 --- a/server/sonar-web/src/main/js/apps/system/styles.css +++ b/server/sonar-web/src/main/js/apps/system/styles.css @@ -21,6 +21,10 @@ padding-bottom: 15px; } +.system-info-health-card .boxed-group-header > .alert { + margin-top: -6px; +} + .system-info-health-card .boxed-group-inner { padding-top: 0; } @@ -30,43 +34,19 @@ } .system-info-health-info { - margin-top: -4px; -} - -.system-info-health-dot { - display: inline-block; - width: 16px; - height: 16px; - margin: 4px; - border-radius: 16px; - box-sizing: border-box; -} - -.system-info-health-dot.GREEN { - background-color: #00aa00; + margin-top: -12px; } -.system-info-health-dot.YELLOW { - background-color: #eabe06; -} -.system-info-health-dot.RED { - background-color: #d4333f; +.system-info-health-info .status-indicator { + position: relative; + top: 8px; } .system-info-health-info .alert { display: inline-block; - position: relative; - top: -8px; -} - -.system-info-health-info.big-dot .system-info-health-dot { - width: 24px; - height: 24px; - margin: 0; - border-radius: 24px; } -.system-info-health-info.no-margin .system-info-health-dot { +.system-info-health-info.no-margin .status-indicator { margin: 0; } diff --git a/server/sonar-web/src/main/js/apps/system/utils.ts b/server/sonar-web/src/main/js/apps/system/utils.ts index 41ad5223544..ae8818edf9d 100644 --- a/server/sonar-web/src/main/js/apps/system/utils.ts +++ b/server/sonar-web/src/main/js/apps/system/utils.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { each, omit, memoize } from 'lodash'; +import { each, omit, memoize, sortBy } from 'lodash'; import { cleanQuery, parseAsArray, @@ -38,6 +38,16 @@ export interface Query { expandedCards: string[]; } +export interface ClusterSysInfo extends SysInfo { + 'Application Nodes': NodeInfo[]; + 'Search Nodes': NodeInfo[]; +} + +export interface StandaloneSysInfo extends SysInfo { + 'Logs Level': string; +} + +export const LOGS_LEVELS = ['INFO', 'DEBUG', 'TRACE']; export const HEALTH_FIELD = 'Health'; export const HEALTHCAUSES_FIELD = 'Health Causes'; @@ -45,10 +55,6 @@ export function ignoreInfoFields(sysInfoObject: SysValueObject): SysValueObject return omit(sysInfoObject, ['Cluster', HEALTH_FIELD, HEALTHCAUSES_FIELD]); } -export function getAppNodes(sysInfoData: SysInfo): NodeInfo[] { - return sysInfoData['Application Nodes']; -} - export function getHealth(sysInfoObject: SysValueObject): HealthType { return sysInfoObject[HEALTH_FIELD] as HealthType; } @@ -57,15 +63,74 @@ export function getHealthCauses(sysInfoObject: SysValueObject): HealthCause[] { return sysInfoObject[HEALTHCAUSES_FIELD] as HealthCause[]; } -export function getMainCardSection(sysInfoData: SysInfo): SysValueObject { - return omit(sysInfoData, ['Application Nodes', 'Search Nodes', 'Settings', 'Statistics']); +export function getLogsLevel(sysInfoObject: SysValueObject): string { + return (sysInfoObject['Logs Level'] || LOGS_LEVELS[0]) as string; +} + +export function getSystemLogsLevel(sysInfoData?: SysInfo): string { + const defaultLevel = LOGS_LEVELS[0]; + if (!sysInfoData) { + return defaultLevel; + } + if (isCluster(sysInfoData)) { + const nodes = sortBy(getAppNodes(sysInfoData as ClusterSysInfo), node => + LOGS_LEVELS.indexOf(getLogsLevel(node)) + ); + return nodes.length > 0 ? getLogsLevel(nodes[nodes.length - 1]) : defaultLevel; + } else { + return getLogsLevel(sysInfoData); + } } export function getNodeName(nodeInfo: NodeInfo): string { return nodeInfo['Name']; } -export function getSearchNodes(sysInfoData: SysInfo): NodeInfo[] { +export function getClusterMainCardSection(sysInfoData: ClusterSysInfo): SysValueObject { + return omit(sysInfoData, ['Application Nodes', 'Search Nodes', 'Settings', 'Statistics']); +} + +export function getStandaloneMainSections(sysInfoData: StandaloneSysInfo): SysValueObject { + return omit(sysInfoData, [ + 'Settings', + 'Statistics', + 'Compute Engine', + 'Compute Engine JVM', + 'Compute Engine JVM Properties', + 'Elasticsearch', + 'Search JVM', + 'Search JVM Properties', + 'Web Database Connectivity', + 'Web JVM', + 'Web JVM Properties' + ]); +} + +export function getStandaloneSecondarySections(sysInfoData: StandaloneSysInfo): SysInfoSection { + return { + Web: { + 'Web Database Connectivity': sysInfoData['Web Database Connectivity'], + 'Web JVM': sysInfoData['Web JVM'], + 'Web JVM Properties': sysInfoData['Web JVM Properties'] + }, + 'Compute Engine': { + ...sysInfoData['Compute Engine'] as SysValueObject, + 'Compute Engine JVM': sysInfoData['Compute Engine JVM'], + 'Compute Engine JVM Properties': sysInfoData['Compute Engine JVM Properties'] + }, + Search: { + Elasticsearch: sysInfoData['Elasticsearch'] as SysValueObject, + 'Search JVM': sysInfoData['Search JVM'], + 'Search JVM Properties': sysInfoData['Search JVM Properties'] + } + }; +} + +export function getAppNodes(sysInfoData: ClusterSysInfo): NodeInfo[] { + return sysInfoData['Application Nodes']; +} + +export function getSearchNodes(sysInfoData: ClusterSysInfo): NodeInfo[] { return sysInfoData['Search Nodes']; } @@ -83,7 +148,7 @@ export function groupSections(sysInfoData: SysValueObject) { } export function isCluster(sysInfoData?: SysInfo): boolean { - return sysInfoData != undefined && sysInfoData['Cluster']; + return sysInfoData != undefined && sysInfoData['Cluster'] === true; } export const parseQuery = memoize((urlQuery: RawQuery): Query => { diff --git a/server/sonar-web/src/main/js/components/common/BranchStatus.css b/server/sonar-web/src/main/js/components/common/BranchStatus.css index 74278d67573..ccfccb351f1 100644 --- a/server/sonar-web/src/main/js/components/common/BranchStatus.css +++ b/server/sonar-web/src/main/js/components/common/BranchStatus.css @@ -3,18 +3,6 @@ text-align: right; } -.branch-status-indicator { - display: block; - width: 8px; - height: 8px; - border-radius: 8px; - margin: 4px 0; -} - -.branch-status-indicator.is-failed { - background-color: #d4333f; -} - -.branch-status-indicator.is-passed { - background-color: #00aa00; +.branch-status .status-indicator { + margin: 0; } diff --git a/server/sonar-web/src/main/js/components/common/BranchStatus.tsx b/server/sonar-web/src/main/js/components/common/BranchStatus.tsx index 40688087cf0..bb9069bc234 100644 --- a/server/sonar-web/src/main/js/components/common/BranchStatus.tsx +++ b/server/sonar-web/src/main/js/components/common/BranchStatus.tsx @@ -18,11 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import { Branch } from '../../app/types'; import Level from '../ui/Level'; import BugIcon from '../icons-components/BugIcon'; import CodeSmellIcon from '../icons-components/CodeSmellIcon'; +import StatusIndicator from './StatusIndicator'; import VulnerabilityIcon from '../icons-components/VulnerabilityIcon'; import { isShortLivingBranch } from '../../helpers/branches'; import './BranchStatus.css'; @@ -41,22 +41,19 @@ export default function BranchStatus({ branch, concise = false }: Props) { const totalIssues = branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells; - const indicatorClassName = classNames('branch-status-indicator', { - 'is-failed': totalIssues > 0, - 'is-passed': totalIssues === 0 - }); + const indicatorColor = totalIssues > 0 ? 'red' : 'green'; return concise ? ( <ul className="list-inline branch-status"> <li>{totalIssues}</li> <li className="spacer-left"> - <i className={indicatorClassName} /> + <StatusIndicator color={indicatorColor} size="small" /> </li> </ul> ) : ( <ul className="list-inline branch-status"> <li className="spacer-right"> - <i className={indicatorClassName} /> + <StatusIndicator color={indicatorColor} size="small" /> </li> <li> {branch.status.bugs} diff --git a/server/sonar-web/src/main/js/components/common/StatusIndicator.css b/server/sonar-web/src/main/js/components/common/StatusIndicator.css new file mode 100644 index 00000000000..26008838cd2 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/StatusIndicator.css @@ -0,0 +1,34 @@ +.status-indicator { + display: inline-block; + box-sizing: border-box; + width: 16px; + height: 16px; + border-radius: 16px; + margin: 4px; +} + +.status-indicator.small-status-indicator { + width: 8px; + height: 8px; + border-radius: 8px; + margin: 8px; +} + +.status-indicator.big-status-indicator { + width: 24px; + height: 24px; + border-radius: 24px; + margin: 0; +} + +.status-indicator.red { + background-color: #d4333f; +} + +.sstatus-indicator.yellow { + background-color: #eabe06; +} + +.status-indicator.green { + background-color: #00aa00; +} diff --git a/server/sonar-web/src/main/js/apps/system/components/StandAloneSysInfos.tsx b/server/sonar-web/src/main/js/components/common/StatusIndicator.tsx index 2a142c58a0e..6fc96354033 100644 --- a/server/sonar-web/src/main/js/apps/system/components/StandAloneSysInfos.tsx +++ b/server/sonar-web/src/main/js/components/common/StatusIndicator.tsx @@ -1,7 +1,7 @@ /* * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 @@ -18,13 +18,27 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; +import './StatusIndicator.css'; interface Props { - sysInfoData: object; + className?: string; + color?: string; + size?: string; } -export default class StandAloneSysInfos extends React.PureComponent<Props> { - render() { - return <div>StandAloneSysInfos</div>; - } +export default function StatusIndicator({ className, color, size }: Props) { + return ( + <i + className={classNames( + 'status-indicator', + color, + { + 'small-status-indicator': size === 'small', + 'big-status-indicator': size === 'big' + }, + className + )} + /> + ); } diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap index b106929fbe4..32c0d0a2afa 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap @@ -23,8 +23,9 @@ exports[`renders status of short-living branches 1`] = ` <li className="spacer-right" > - <i - className="branch-status-indicator is-passed" + <StatusIndicator + color="green" + size="small" /> </li> <li> @@ -49,8 +50,9 @@ exports[`renders status of short-living branches 2`] = ` <li className="spacer-right" > - <i - className="branch-status-indicator is-failed" + <StatusIndicator + color="red" + size="small" /> </li> <li> @@ -75,8 +77,9 @@ exports[`renders status of short-living branches 3`] = ` <li className="spacer-right" > - <i - className="branch-status-indicator is-failed" + <StatusIndicator + color="red" + size="small" /> </li> <li> diff --git a/server/sonar-web/src/main/js/components/icons-components/OpenCloseIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/OpenCloseIcon.tsx index 739bc6027e7..ba583b3de7c 100644 --- a/server/sonar-web/src/main/js/components/icons-components/OpenCloseIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons-components/OpenCloseIcon.tsx @@ -35,9 +35,9 @@ export default function OpenCloseIcon({ className, open, size = 14 }: Props) { height={size} style={{ fill: 'currentColor' }}> {open ? ( - <path d="M13.506 8.539l-5.191 5.184q-0.133 0.133-0.315 0.133t-0.315-0.133l-5.191-5.184q-0.133-0.133-0.133-0.318t0.133-0.318l1.161-1.154q0.133-0.133 0.315-0.133t0.315 0.133l3.715 3.715 3.715-3.715q0.133-0.133 0.315-0.133t0.315 0.133l1.161 1.154q0.133 0.133 0.133 0.318t-0.133 0.318z" /> + <path d="M13.506 9.289l-5.191 5.184q-0.133 0.133-0.315 0.133t-0.315-0.133l-5.191-5.184q-0.133-0.133-0.133-0.318t0.133-0.318l1.161-1.154q0.133-0.133 0.315-0.133t0.315 0.133l3.715 3.715 3.715-3.715q0.133-0.133 0.315-0.133t0.315 0.133l1.161 1.154q0.133 0.133 0.133 0.318t-0.133 0.318z" /> ) : ( - <path d="M13.527 8.318l-5.244 5.244q-0.134 0.134-0.318 0.134t-0.318-0.134l-1.173-1.173q-0.134-0.134-0.134-0.318t0.134-0.318l3.753-3.753-3.753-3.753q-0.134-0.134-0.134-0.318t0.134-0.318l1.173-1.173q0.134-0.134 0.318-0.134t0.318 0.134l5.244 5.244q0.134 0.134 0.134 0.318t-0.134 0.318z" /> + <path d="M13.527 9.318l-5.244 5.244q-0.134 0.134-0.318 0.134t-0.318-0.134l-1.173-1.173q-0.134-0.134-0.134-0.318t0.134-0.318l3.753-3.753-3.753-3.753q-0.134-0.134-0.134-0.318t0.134-0.318l1.173-1.173q0.134-0.134 0.318-0.134t0.318 0.134l5.244 5.244q0.134 0.134 0.134 0.318t-0.134 0.318z" /> )} </svg> ); diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ef96a4360d5..377cec174a1 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2847,7 +2847,8 @@ system.cluster_log_level.info=Changes apply to all Application nodes but not to system.download_logs=Download Logs system.download_system_info=Download System Info system.is_restarting=Server is restarting. This page will be automatically refreshed. -system.log_level.warning=Current level has performance impacts, please make sure to get back to INFO level once your investigation is done. Please note that when the server is restarted, logging will revert to the level configured in sonar.properties. +system.log_level.warning=This level has performance impacts, please make sure to get back to INFO level once your investigation is done. Please note that when the server is restarted, logging will revert to the level configured in sonar.properties. +system.log_level.warning.short=Current logs level has performance impacts, get back to INFO level. system.log_level.info=Changes don't apply to Search. system.logs_level=Logs level system.restart_server=Restart Server |