]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9802 allow to change the log level of a cluster
authorDaniel Schwarz <daniel.schwarz@sonarsource.com>
Mon, 18 Sep 2017 11:28:57 +0000 (13:28 +0200)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Tue, 26 Sep 2017 21:49:38 +0000 (23:49 +0200)
19 files changed:
server/sonar-ce/src/main/java/org/sonar/ce/logging/ChangeLogLevelHttpAction.java
server/sonar-ce/src/test/java/org/sonar/ce/logging/ChangeLogLevelHttpActionTest.java
server/sonar-process/src/main/java/org/sonar/process/cluster/hz/DistributedAnswer.java
server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberSelectors.java [new file with mode: 0644]
server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberSelectorsTest.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/ServerLogging.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelAction.java
server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelClusterService.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelService.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelStandaloneService.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/platform/ServerLoggingTest.java
server/sonar-server/src/test/java/org/sonar/server/platform/platformlevel/PlatformLevelTest.java
server/sonar-server/src/test/java/org/sonar/server/platform/ws/ChangeLogLevelActionTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/system/SystemService.java
sonar-ws/src/test/java/org/sonarqube/ws/client/system/SystemServiceTest.java
tests/src/test/java/org/sonarqube/tests/cluster/ClusterTest.java
tests/src/test/java/org/sonarqube/tests/cluster/Node.java

index 3e47ea2a9629e14a495411b9b444c82f3014b7df..aa67614d5885ec1e2ac8984ed3e21be7b0458f57 100644 (file)
@@ -23,8 +23,6 @@ import fi.iki.elonen.NanoHTTPD;
 import org.sonar.api.utils.log.LoggerLevel;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.ce.httpd.HttpAction;
-import org.sonar.ce.log.CeProcessLogging;
-import org.sonar.db.Database;
 import org.sonar.server.platform.ServerLogging;
 
 import static fi.iki.elonen.NanoHTTPD.MIME_PLAINTEXT;
@@ -40,13 +38,9 @@ public class ChangeLogLevelHttpAction implements HttpAction {
   private static final String PARAM_LEVEL = "level";
 
   private final ServerLogging logging;
-  private final Database db;
-  private final CeProcessLogging ceProcessLogging;
 
-  public ChangeLogLevelHttpAction(ServerLogging logging, Database db, CeProcessLogging ceProcessLogging) {
+  public ChangeLogLevelHttpAction(ServerLogging logging) {
     this.logging = logging;
-    this.db = db;
-    this.ceProcessLogging = ceProcessLogging;
   }
 
   @Override
@@ -66,8 +60,7 @@ public class ChangeLogLevelHttpAction implements HttpAction {
     }
     try {
       LoggerLevel level = LoggerLevel.valueOf(levelStr);
-      db.enableSqlLogging(level.equals(LoggerLevel.TRACE));
-      logging.changeLevel(ceProcessLogging, level);
+      logging.changeLevel(level);
       return newFixedLengthResponse(OK, MIME_PLAINTEXT, null);
     } catch (IllegalArgumentException e) {
       Loggers.get(ChangeLogLevelHttpAction.class).debug("Value '{}' for parameter '{}' is invalid", levelStr, PARAM_LEVEL, e);
index 88435a2a8ba76ae3939d713538a3369a1888d21e..40a1a5ce66f60bb27b3b185740b918ca28ffa8e9 100644 (file)
@@ -26,8 +26,6 @@ import org.apache.commons.io.IOUtils;
 import org.junit.Test;
 import org.sonar.api.utils.log.LoggerLevel;
 import org.sonar.ce.httpd.HttpAction;
-import org.sonar.ce.log.CeProcessLogging;
-import org.sonar.db.Database;
 import org.sonar.server.platform.ServerLogging;
 
 import static fi.iki.elonen.NanoHTTPD.Method.GET;
@@ -42,9 +40,7 @@ import static org.sonar.ce.httpd.CeHttpUtils.createHttpSession;
 
 public class ChangeLogLevelHttpActionTest {
   private ServerLogging serverLogging = mock(ServerLogging.class);
-  private Database database = mock(Database.class);
-  private CeProcessLogging ceProcessLogging = new CeProcessLogging();
-  private ChangeLogLevelHttpAction underTest = new ChangeLogLevelHttpAction(serverLogging, database, ceProcessLogging);
+  private ChangeLogLevelHttpAction underTest = new ChangeLogLevelHttpAction(serverLogging);
 
   @Test
   public void register_to_path_changeLogLevel() {
@@ -78,42 +74,38 @@ public class ChangeLogLevelHttpActionTest {
   }
 
   @Test
-  public void changes_server_logging_and_disabled_database_logging_if_level_is_ERROR() {
+  public void changes_server_logging_if_level_is_ERROR() {
     NanoHTTPD.Response response = underTest.serve(createHttpSession(POST, ImmutableMap.of("level", "ERROR")));
 
     assertThat(response.getStatus()).isEqualTo(OK);
 
-    verify(serverLogging).changeLevel(ceProcessLogging, LoggerLevel.ERROR);
-    verify(database).enableSqlLogging(false);
+    verify(serverLogging).changeLevel(LoggerLevel.ERROR);
   }
 
   @Test
-  public void changes_server_logging_and_disabled_database_logging_if_level_is_INFO() {
+  public void changes_server_logging_if_level_is_INFO() {
     NanoHTTPD.Response response = underTest.serve(createHttpSession(POST, ImmutableMap.of("level", "INFO")));
 
     assertThat(response.getStatus()).isEqualTo(OK);
 
-    verify(serverLogging).changeLevel(ceProcessLogging, LoggerLevel.INFO);
-    verify(database).enableSqlLogging(false);
+    verify(serverLogging).changeLevel(LoggerLevel.INFO);
   }
 
   @Test
-  public void changes_server_logging_and_disabled_database_logging_if_level_is_DEBUG() {
+  public void changes_server_logging_if_level_is_DEBUG() {
     NanoHTTPD.Response response = underTest.serve(createHttpSession(POST, ImmutableMap.of("level", "DEBUG")));
 
     assertThat(response.getStatus()).isEqualTo(OK);
 
-    verify(serverLogging).changeLevel(ceProcessLogging, LoggerLevel.DEBUG);
-    verify(database).enableSqlLogging(false);
+    verify(serverLogging).changeLevel(LoggerLevel.DEBUG);
   }
 
   @Test
-  public void changes_server_logging_and_enable_database_logging_if_level_is_TRACE() {
+  public void changes_server_logging_if_level_is_TRACE() {
     NanoHTTPD.Response response = underTest.serve(createHttpSession(POST, ImmutableMap.of("level", "TRACE")));
 
     assertThat(response.getStatus()).isEqualTo(OK);
 
-    verify(serverLogging).changeLevel(ceProcessLogging, LoggerLevel.TRACE);
-    verify(database).enableSqlLogging(true);
+    verify(serverLogging).changeLevel(LoggerLevel.TRACE);
   }
 }
index be6105cc511887e6cba8c4f68e0c5cd451926cc8..f5a0bc920c87319977871d754867618ee6a5c37a 100644 (file)
@@ -28,6 +28,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Answer of {@link DistributedCall}, aggregating the answers from
@@ -70,4 +71,21 @@ public class DistributedAnswer<T> {
   public void setFailed(Member member, Exception e) {
     failedMembers.put(member, e);
   }
+
+  public void propagateExceptions() {
+    if (!failedMembers.isEmpty()) {
+      String failedMemberNames = failedMembers.keySet().stream()
+        .map(m -> m.getStringAttribute(HazelcastMember.Attribute.NODE_NAME))
+        .collect(Collectors.joining(", "));
+      throw new IllegalStateException("Distributed cluster action in cluster nodes " + failedMemberNames + " (other nodes may have timed out)",
+        failedMembers.values().iterator().next());
+    }
+
+    if (!timedOutMembers.isEmpty()) {
+      String timedOutMemberNames = timedOutMembers.stream()
+        .map(m -> m.getStringAttribute(HazelcastMember.Attribute.NODE_NAME))
+        .collect(Collectors.joining(", "));
+      throw new IllegalStateException("Distributed cluster action timed out in cluster nodes " + timedOutMemberNames);
+    }
+  }
 }
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberSelectors.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberSelectors.java
new file mode 100644 (file)
index 0000000..3b7abe5
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.process.cluster.hz;
+
+import com.hazelcast.core.MemberSelector;
+import java.util.List;
+import org.sonar.process.ProcessId;
+
+import static java.util.Arrays.asList;
+import static org.sonar.process.ProcessId.fromKey;
+
+public class HazelcastMemberSelectors {
+
+  private HazelcastMemberSelectors() {
+  }
+
+  public static MemberSelector selectorForProcessIds(ProcessId... processIds) {
+    List<ProcessId> processIdList = asList(processIds);
+    return member -> {
+      ProcessId memberProcessId = fromKey(member.getStringAttribute(HazelcastMember.Attribute.PROCESS_KEY));
+      return processIdList.contains(memberProcessId);
+    };
+  }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberSelectorsTest.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberSelectorsTest.java
new file mode 100644 (file)
index 0000000..309e346
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * 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.process.cluster.hz;
+
+import com.hazelcast.core.Member;
+import com.hazelcast.core.MemberSelector;
+import org.junit.Test;
+
+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.ProcessId.APP;
+import static org.sonar.process.ProcessId.COMPUTE_ENGINE;
+import static org.sonar.process.ProcessId.WEB_SERVER;
+import static org.sonar.process.cluster.hz.HazelcastMember.Attribute.PROCESS_KEY;
+
+public class HazelcastMemberSelectorsTest {
+
+  @Test
+  public void selecting_ce_nodes() throws Exception {
+    Member member = mock(Member.class);
+    MemberSelector underTest = HazelcastMemberSelectors.selectorForProcessIds(COMPUTE_ENGINE);
+
+    when(member.getStringAttribute(PROCESS_KEY)).thenReturn(COMPUTE_ENGINE.getKey());
+    assertThat(underTest.select(member)).isTrue();
+
+    when(member.getStringAttribute(PROCESS_KEY)).thenReturn(WEB_SERVER.getKey());
+    assertThat(underTest.select(member)).isFalse();
+
+    when(member.getStringAttribute(PROCESS_KEY)).thenReturn(APP.getKey());
+    assertThat(underTest.select(member)).isFalse();
+  }
+
+  @Test
+  public void selecting_web_and_app_nodes() throws Exception {
+    Member member = mock(Member.class);
+    MemberSelector underTest = HazelcastMemberSelectors.selectorForProcessIds(WEB_SERVER, APP);
+
+    when(member.getStringAttribute(PROCESS_KEY)).thenReturn(COMPUTE_ENGINE.getKey());
+    assertThat(underTest.select(member)).isFalse();
+
+    when(member.getStringAttribute(PROCESS_KEY)).thenReturn(WEB_SERVER.getKey());
+    assertThat(underTest.select(member)).isTrue();
+
+    when(member.getStringAttribute(PROCESS_KEY)).thenReturn(APP.getKey());
+    assertThat(underTest.select(member)).isTrue();
+  }
+}
\ No newline at end of file
index a60631aed04e8b452a80581ae908ff76cf1cecd1..c7da8f500859493f7bf154b884261de2cfde3c7c 100644 (file)
@@ -23,35 +23,60 @@ import ch.qos.logback.classic.Level;
 import ch.qos.logback.classic.Logger;
 import com.google.common.annotations.VisibleForTesting;
 import java.io.File;
+import org.picocontainer.Startable;
 import org.slf4j.LoggerFactory;
 import org.sonar.api.ce.ComputeEngineSide;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.server.ServerSide;
 import org.sonar.api.utils.log.LoggerLevel;
 import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.Database;
 import org.sonar.process.ProcessProperties;
 import org.sonar.process.logging.LogbackHelper;
 import org.sonar.server.app.ServerProcessLogging;
 
+import static org.sonar.api.utils.log.LoggerLevel.TRACE;
+
 @ServerSide
 @ComputeEngineSide
-public class ServerLogging {
+public class ServerLogging implements Startable {
 
+  /** Used for Hazelcast's distributed queries in cluster mode */
+  private static ServerLogging INSTANCE;
   private final LogbackHelper helper;
   private final Configuration config;
+  private final ServerProcessLogging serverProcessLogging;
+  private final Database database;
 
-  public ServerLogging(Configuration config) {
-    this(new LogbackHelper(), config);
+  public ServerLogging(Configuration config, ServerProcessLogging serverProcessLogging, Database database) {
+    this(new LogbackHelper(), config, serverProcessLogging, database);
   }
 
   @VisibleForTesting
-  ServerLogging(LogbackHelper helper, Configuration config) {
+  ServerLogging(LogbackHelper helper, Configuration config, ServerProcessLogging serverProcessLogging, Database database) {
     this.helper = helper;
     this.config = config;
+    this.serverProcessLogging = serverProcessLogging;
+    this.database = database;
+  }
+
+  @Override
+  public void start() {
+    INSTANCE = this;
+  }
+
+  @Override
+  public void stop() {
+    INSTANCE = null;
+  }
+
+  public static void changeLevelFromHazelcastDistributedQuery(LoggerLevel level) {
+    INSTANCE.changeLevel(level);
   }
 
-  public void changeLevel(ServerProcessLogging serverProcessLogging, LoggerLevel level) {
+  public void changeLevel(LoggerLevel level) {
     Level logbackLevel = Level.toLevel(level.name());
+    database.enableSqlLogging(level == TRACE);
     helper.changeRoot(serverProcessLogging.getLogLevelConfig(), logbackLevel);
     LoggerFactory.getLogger(ServerLogging.class).info("Level of logs changed to {}", level);
   }
index cd09c49490b4cc7f23de0278c68534c296d5841f..61bea5df8be1dcafb6fe0716ff56f1c899c6f6ef 100644 (file)
@@ -38,6 +38,7 @@ public abstract class PlatformLevel {
   private final ComponentContainer container;
   private AddIfStartupLeader addIfStartupLeader;
   private AddIfCluster addIfCluster;
+  private AddIfStandalone addIfStandalone;
 
   public PlatformLevel(String name) {
     this.name = name;
@@ -156,6 +157,19 @@ public abstract class PlatformLevel {
     return addIfCluster;
   }
 
+  /**
+   * Add a component to container only if this is a standalone instance, without clustering.
+   *
+   * @throws IllegalStateException if called from PlatformLevel1, when cluster settings are not loaded
+   */
+  AddIfStandalone addIfStandalone(Object... objects) {
+    if (addIfStandalone == null) {
+      addIfStandalone = new AddIfStandalone(getWebServer().isStandalone());
+    }
+    addIfStandalone.ifAdd(objects);
+    return addIfStandalone;
+  }
+
   private WebServer getWebServer() {
     return getOptional(WebServer.class)
       .orElseThrow(() -> new IllegalStateException("WebServer not available in Pico yet"));
@@ -193,6 +207,12 @@ public abstract class PlatformLevel {
     }
   }
 
+  public final class AddIfStandalone extends AddIf {
+    private AddIfStandalone(boolean condition) {
+      super(condition);
+    }
+  }
+
   protected void addAll(Collection<?> objects) {
     add(objects.toArray(new Object[objects.size()]));
   }
index 5d015b81036d3160aea82d7db74ff31d5347c917..0ef1562a4898bd412738fb8bebd960c2e04a6f4e 100644 (file)
@@ -112,6 +112,8 @@ import org.sonar.server.platform.monitoring.WebSystemInfoModule;
 import org.sonar.server.platform.web.WebPagesFilter;
 import org.sonar.server.platform.web.requestid.HttpRequestIdModule;
 import org.sonar.server.platform.ws.ChangeLogLevelAction;
+import org.sonar.server.platform.ws.ChangeLogLevelClusterService;
+import org.sonar.server.platform.ws.ChangeLogLevelStandaloneService;
 import org.sonar.server.platform.ws.DbMigrationStatusAction;
 import org.sonar.server.platform.ws.HealthActionModule;
 import org.sonar.server.platform.ws.L10nWs;
@@ -243,7 +245,10 @@ public class PlatformLevel4 extends PlatformLevel {
 
     addIfCluster(
       StartableHazelcastMember.class,
-      NodeHealthModule.class);
+      NodeHealthModule.class,
+      ChangeLogLevelClusterService.class);
+    addIfStandalone(
+      ChangeLogLevelStandaloneService.class);
 
     add(
       PluginDownloader.class,
index 9bc42601479ccd6ea38d9bd270e14a3827a5b6ea..a62a78962ecb2d9761126f4ed39a1f35196ff569 100644 (file)
@@ -23,10 +23,6 @@ import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.log.LoggerLevel;
-import org.sonar.ce.http.CeHttpClient;
-import org.sonar.db.Database;
-import org.sonar.server.app.WebServerProcessLogging;
-import org.sonar.server.platform.ServerLogging;
 import org.sonar.server.user.UserSession;
 
 import static org.sonar.process.logging.LogbackHelper.allowedLogLevels;
@@ -36,17 +32,11 @@ public class ChangeLogLevelAction implements SystemWsAction {
   private static final String PARAM_LEVEL = "level";
 
   private final UserSession userSession;
-  private final ServerLogging logging;
-  private final Database db;
-  private final CeHttpClient ceHttpClient;
-  private final WebServerProcessLogging webServerProcessLogging;
+  private final ChangeLogLevelService service;
 
-  public ChangeLogLevelAction(UserSession userSession, ServerLogging logging, Database db, CeHttpClient ceHttpClient, WebServerProcessLogging webServerProcessLogging) {
+  public ChangeLogLevelAction(UserSession userSession, ChangeLogLevelService service) {
     this.userSession = userSession;
-    this.logging = logging;
-    this.db = db;
-    this.ceHttpClient = ceHttpClient;
-    this.webServerProcessLogging = webServerProcessLogging;
+    this.service = service;
   }
 
   @Override
@@ -69,9 +59,7 @@ public class ChangeLogLevelAction implements SystemWsAction {
     userSession.checkIsSystemAdministrator();
 
     LoggerLevel level = LoggerLevel.valueOf(wsRequest.mandatoryParam(PARAM_LEVEL));
-    db.enableSqlLogging(level.equals(LoggerLevel.TRACE));
-    logging.changeLevel(webServerProcessLogging, level);
-    ceHttpClient.changeLogLevel(level);
+    service.changeLogLevel(level);
     wsResponse.noContent();
   }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelClusterService.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelClusterService.java
new file mode 100644 (file)
index 0000000..362fa62
--- /dev/null
@@ -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.
+ */
+package org.sonar.server.platform.ws;
+
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.process.ProcessId;
+import org.sonar.process.cluster.hz.DistributedCall;
+import org.sonar.process.cluster.hz.HazelcastMember;
+import org.sonar.process.cluster.hz.HazelcastMemberSelectors;
+import org.sonar.server.platform.ServerLogging;
+
+public class ChangeLogLevelClusterService implements ChangeLogLevelService {
+
+  private static final long CLUSTER_TIMEOUT_MILLIS = 5000;
+  private static final Logger LOGGER = Loggers.get(ChangeLogLevelClusterService.class);
+
+  private final HazelcastMember member;
+
+  public ChangeLogLevelClusterService(HazelcastMember member) {
+    this.member = member;
+  }
+
+  public void changeLogLevel(LoggerLevel level) {
+    try {
+      member.call(setLogLevelForNode(level), HazelcastMemberSelectors.selectorForProcessIds(ProcessId.WEB_SERVER, ProcessId.COMPUTE_ENGINE), CLUSTER_TIMEOUT_MILLIS)
+        .propagateExceptions();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+    }
+  }
+
+  private static DistributedCall<Object> setLogLevelForNode(LoggerLevel level) {
+    return () -> {
+      try {
+        ServerLogging.changeLevelFromHazelcastDistributedQuery(level);
+      } catch (Exception e) {
+        LOGGER.error("Setting log level to '" + level.name() + "' in this cluster node failed", e);
+        throw new IllegalStateException("Setting log level to '" + level.name() + "' in this cluster node failed", e);
+      }
+      return null;
+    };
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelService.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelService.java
new file mode 100644 (file)
index 0000000..1ef59c7
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform.ws;
+
+import org.sonar.api.utils.log.LoggerLevel;
+
+public interface ChangeLogLevelService {
+
+  void changeLogLevel(LoggerLevel level);
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelStandaloneService.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelStandaloneService.java
new file mode 100644 (file)
index 0000000..fc551a0
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform.ws;
+
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.ce.http.CeHttpClient;
+import org.sonar.server.platform.ServerLogging;
+
+public class ChangeLogLevelStandaloneService implements ChangeLogLevelService {
+
+  private final ServerLogging logging;
+  private final CeHttpClient ceHttpClient;
+
+  public ChangeLogLevelStandaloneService(ServerLogging logging, CeHttpClient ceHttpClient) {
+    this.logging = logging;
+    this.ceHttpClient = ceHttpClient;
+  }
+
+  public void changeLogLevel(LoggerLevel level) {
+    logging.changeLevel(level);
+    ceHttpClient.changeLogLevel(level);
+  }
+}
index 05fec294c8b8a3f0c699b675652d3ce752cf8342..440e7290fe73e56bf0c18fd39046c5ab5321ede7 100644 (file)
@@ -34,6 +34,7 @@ import org.junit.runner.RunWith;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.utils.log.LogTester;
 import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.db.Database;
 import org.sonar.process.ProcessProperties;
 import org.sonar.process.logging.LogLevelConfig;
 import org.sonar.process.logging.LogbackHelper;
@@ -41,9 +42,15 @@ import org.sonar.server.app.ServerProcessLogging;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.sonar.api.utils.log.LoggerLevel.DEBUG;
+import static org.sonar.api.utils.log.LoggerLevel.ERROR;
+import static org.sonar.api.utils.log.LoggerLevel.INFO;
+import static org.sonar.api.utils.log.LoggerLevel.TRACE;
+import static org.sonar.api.utils.log.LoggerLevel.WARN;
 
 @RunWith(DataProviderRunner.class)
 public class ServerLoggingTest {
@@ -56,7 +63,9 @@ public class ServerLoggingTest {
   private final String rootLoggerName = RandomStringUtils.randomAlphabetic(20);
   private LogbackHelper logbackHelper = spy(new LogbackHelper());
   private MapSettings settings = new MapSettings();
-  private ServerLogging underTest = new ServerLogging(logbackHelper, settings.asConfig());
+  private final ServerProcessLogging serverProcessLogging = mock(ServerProcessLogging.class);
+  private final Database database = mock(Database.class);
+  private ServerLogging underTest = new ServerLogging(logbackHelper, settings.asConfig(), serverProcessLogging, database);
 
   @Rule
   public LogTester logTester = new LogTester();
@@ -71,28 +80,45 @@ public class ServerLoggingTest {
 
   @Test
   public void getRootLoggerLevel() {
-    logTester.setLevel(LoggerLevel.TRACE);
-    assertThat(underTest.getRootLoggerLevel()).isEqualTo(LoggerLevel.TRACE);
+    logTester.setLevel(TRACE);
+    assertThat(underTest.getRootLoggerLevel()).isEqualTo(TRACE);
   }
 
   @Test
   @UseDataProvider("supportedSonarApiLevels")
   public void changeLevel_calls_changeRoot_with_LogLevelConfig_and_level_converted_to_logback_class_then_log_INFO_message(LoggerLevel level) {
-    ServerProcessLogging serverProcessLogging = mock(ServerProcessLogging.class);
     LogLevelConfig logLevelConfig = LogLevelConfig.newBuilder(rootLoggerName).build();
     when(serverProcessLogging.getLogLevelConfig()).thenReturn(logLevelConfig);
 
-    underTest.changeLevel(serverProcessLogging, level);
+    underTest.changeLevel(level);
 
     verify(logbackHelper).changeRoot(logLevelConfig, Level.valueOf(level.name()));
   }
 
+  @Test
+  public void changeLevel_to_trace_enables_db_logging() {
+    LogLevelConfig logLevelConfig = LogLevelConfig.newBuilder(rootLoggerName).build();
+    when(serverProcessLogging.getLogLevelConfig()).thenReturn(logLevelConfig);
+
+    reset(database);
+    underTest.changeLevel(INFO);
+    verify(database).enableSqlLogging(false);
+
+    reset(database);
+    underTest.changeLevel(DEBUG);
+    verify(database).enableSqlLogging(false);
+
+    reset(database);
+    underTest.changeLevel(TRACE);
+    verify(database).enableSqlLogging(true);
+  }
+
   @DataProvider
   public static Object[][] supportedSonarApiLevels() {
     return new Object[][] {
-      {LoggerLevel.INFO},
-      {LoggerLevel.DEBUG},
-      {LoggerLevel.TRACE}
+      {INFO},
+      {DEBUG},
+      {TRACE}
     };
   }
 
@@ -101,7 +127,7 @@ public class ServerLoggingTest {
     expectedException.expect(IllegalArgumentException.class);
     expectedException.expectMessage("ERROR log level is not supported (allowed levels are [TRACE, DEBUG, INFO])");
 
-    underTest.changeLevel(mock(ServerProcessLogging.class), LoggerLevel.ERROR);
+    underTest.changeLevel(ERROR);
   }
 
   @Test
@@ -109,6 +135,6 @@ public class ServerLoggingTest {
     expectedException.expect(IllegalArgumentException.class);
     expectedException.expectMessage("WARN log level is not supported (allowed levels are [TRACE, DEBUG, INFO])");
 
-    underTest.changeLevel(mock(ServerProcessLogging.class), LoggerLevel.WARN);
+    underTest.changeLevel(WARN);
   }
 }
index ae60e8dbdb0d2ddd3d133ef54c0dc766f5c475c6..8f039e9f41153f1a96e15203754ba9d9c815736d 100644 (file)
@@ -72,4 +72,20 @@ public class PlatformLevelTest {
     PlatformLevel.AddIfCluster addIfCluster = underTest.addIfCluster();
     IntStream.range(0, 1 + new Random().nextInt(4)).forEach(i -> assertThat(underTest.addIfCluster()).isSameAs(addIfCluster));
   }
+
+  @Test
+  public void addIfStandalone_throws_ISE_if_container_does_not_have_WebServer_object() {
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("WebServer not available in Pico yet");
+
+    underTest.addIfCluster();
+  }
+
+  @Test
+  public void addIfStandalone_always_returns_the_same_instance() {
+    underTest.add(Mockito.mock(WebServer.class));
+
+    PlatformLevel.AddIfCluster addIfCluster = underTest.addIfCluster();
+    IntStream.range(0, 1 + new Random().nextInt(4)).forEach(i -> assertThat(underTest.addIfCluster()).isSameAs(addIfCluster));
+  }
 }
index 3650dc10d7b84b90a6c53561cbd0891a1cb0d87f..b19317404920cb8e887b4f1c337ddc50224fe16f 100644 (file)
@@ -25,8 +25,6 @@ import org.junit.rules.ExpectedException;
 import org.sonar.api.utils.log.LoggerLevel;
 import org.sonar.ce.http.CeHttpClient;
 import org.sonar.ce.http.CeHttpClientImpl;
-import org.sonar.db.Database;
-import org.sonar.server.app.WebServerProcessLogging;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.platform.ServerLogging;
 import org.sonar.server.tester.UserSessionRule;
@@ -43,10 +41,8 @@ public class ChangeLogLevelActionTest {
   public ExpectedException expectedException = ExpectedException.none();
 
   private ServerLogging serverLogging = mock(ServerLogging.class);
-  private Database db = mock(Database.class);
   private CeHttpClient ceHttpClient = mock(CeHttpClientImpl.class);
-  private WebServerProcessLogging webServerProcessLogging = new WebServerProcessLogging();
-  private ChangeLogLevelAction underTest = new ChangeLogLevelAction(userSession, serverLogging, db, ceHttpClient, webServerProcessLogging);
+  private ChangeLogLevelAction underTest = new ChangeLogLevelAction(userSession, new ChangeLogLevelStandaloneService(serverLogging, ceHttpClient));
   private WsActionTester actionTester = new WsActionTester(underTest);
 
   @Test
@@ -74,9 +70,8 @@ public class ChangeLogLevelActionTest {
       .setMethod("POST")
       .execute();
 
-    verify(serverLogging).changeLevel(webServerProcessLogging, LoggerLevel.DEBUG);
+    verify(serverLogging).changeLevel(LoggerLevel.DEBUG);
     verify(ceHttpClient).changeLogLevel(LoggerLevel.DEBUG);
-    verify(db).enableSqlLogging(false);
   }
 
   @Test
@@ -88,9 +83,8 @@ public class ChangeLogLevelActionTest {
       .setMethod("POST")
       .execute();
 
-    verify(serverLogging).changeLevel(webServerProcessLogging, LoggerLevel.TRACE);
+    verify(serverLogging).changeLevel(LoggerLevel.TRACE);
     verify(ceHttpClient).changeLogLevel(LoggerLevel.TRACE);
-    verify(db).enableSqlLogging(true);
   }
 
   @Test
index cb8c55aec6197e805dcb539795d55a48353e9bd5..916072d215771d62269bae242210be1b183dbf55 100644 (file)
@@ -24,6 +24,7 @@ import org.sonarqube.ws.client.BaseService;
 import org.sonarqube.ws.client.GetRequest;
 import org.sonarqube.ws.client.PostRequest;
 import org.sonarqube.ws.client.WsConnector;
+import org.sonarqube.ws.client.WsResponse;
 
 public class SystemService extends BaseService {
   public SystemService(WsConnector wsConnector) {
@@ -41,4 +42,12 @@ public class SystemService extends BaseService {
   public WsSystem.StatusResponse status() {
     return call(new GetRequest(path("status")), WsSystem.StatusResponse.parser());
   }
+
+  public void changeLogLevel(String level) {
+    call(new PostRequest(path("change_log_level")).setParam("level", level));
+  }
+
+  public WsResponse info() {
+    return call(new GetRequest(path("info")));
+  }
 }
index a473bda635ef9ecc4b1cd7120a0e48a946b5a443..145642a527870e9b18e25bc3188351931abad421 100644 (file)
@@ -64,4 +64,25 @@ public class SystemServiceTest {
         .hasPath("restart")
         .andNoOtherParam();
   }
+
+  @Test
+  public void test_changeLogLevel() throws Exception {
+    underTest.changeLogLevel("TRACE");
+
+    PostRequest postRequest = serviceTester.getPostRequest();
+    serviceTester.assertThat(postRequest)
+        .hasPath("change_log_level")
+        .hasParam("level", "TRACE")
+        .andNoOtherParam();
+  }
+
+  @Test
+  public void test_info() throws Exception {
+    underTest.info();
+
+    GetRequest getRequest = serviceTester.getGetRequest();
+    serviceTester.assertThat(getRequest)
+        .hasPath("info")
+        .andNoOtherParam();
+  }
 }
index 2fab9bedb0b17697555ab86182a95b3ae75becce..a6afc2ca1fba6aa8c8cdb75d1869e35512cd33ea 100644 (file)
  */
 package org.sonarqube.tests.cluster;
 
+import com.google.gson.internal.LinkedTreeMap;
 import com.sonar.orchestrator.Orchestrator;
 import com.sonar.orchestrator.OrchestratorBuilder;
 import com.sonar.orchestrator.db.DefaultDatabase;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.function.BinaryOperator;
 import java.util.function.Consumer;
 import java.util.stream.IntStream;
@@ -38,8 +40,9 @@ import org.junit.rules.TemporaryFolder;
 import org.junit.rules.TestRule;
 import org.junit.rules.Timeout;
 import org.sonarqube.ws.WsSystem;
-import org.sonarqube.ws.client.GetRequest;
 import org.sonarqube.ws.client.HttpException;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import util.ItUtils;
 
 import static com.google.common.base.Preconditions.checkState;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -70,24 +73,6 @@ public class ClusterTest {
     db.stop();
   }
 
-  /**
-   * TODO WIP
-   */
-  @Test
-  public void wip() throws Exception {
-    try (Cluster cluster = newCluster(3, 2)) {
-      cluster.getNodes().forEach(Node::start);
-
-      Node app = cluster.getAppNode(0);
-      app.waitForHealthGreen();
-
-      System.out.println("-----------------------------------------------------------------------");
-      String json = app.wsClient().wsConnector().call(new GetRequest("api/system/info")).content();
-      System.out.println(json);
-      System.out.println("-----------------------------------------------------------------------");
-    }
-  }
-
   @Test
   public void test_high_availability_topology() throws Exception {
     try (Cluster cluster = newCluster(3, 2)) {
@@ -264,6 +249,37 @@ public class ClusterTest {
     }
   }
 
+  @Test
+  public void set_log_level_affects_all_nodes() throws Exception {
+    try (Cluster cluster = newCluster(2, 2)) {
+      cluster.getNodes().forEach(Node::start);
+      cluster.getAppNodes().forEach(Node::waitForStatusUp);
+
+      cluster.getAppNodes().forEach(node -> {
+        assertThat(node.webLogsContain(" TRACE web[")).isFalse();
+      });
+
+      cluster.getAppNode(0).wsClient().system().changeLogLevel("TRACE");
+
+      cluster.getAppNodes().forEach(node -> {
+
+        // do something, that will produce logging
+        node.wsClient().issues().search(new SearchWsRequest());
+
+        // check logs
+        assertThat(node.webLogsContain(" TRACE web[")).isTrue();
+      });
+
+      Map<String, Object> data = ItUtils.jsonToMap(cluster.getAppNode(0).wsClient().system().info().content());
+      ArrayList<Object> applicationNodes = (ArrayList<Object>) data.get("Application Nodes");
+      applicationNodes.forEach(node -> {
+        LinkedTreeMap<Object, Object> nodeData = (LinkedTreeMap<Object, Object>) node;
+        LinkedTreeMap<Object, Object> ceLoggingData = (LinkedTreeMap<Object, Object>) nodeData.get("Compute Engine Logging");
+        assertThat(ceLoggingData.get("Logs Level")).as("Compute engine logs level of a node").isEqualTo("TRACE");
+      });
+    }
+  }
+
   @Test
   public void restart_action_is_not_allowed_for_cluster_nodes() throws Exception {
     try (Cluster cluster = newCluster(2, 1)) {
index e8846c280429a5ecc8f4e1c9bd0a2db5b247cc62..5b684ac4a244222184e9ece0b2d11233d176e81a 100644 (file)
@@ -207,7 +207,7 @@ class Node {
     return content.hasText(message);
   }
 
-  private boolean webLogsContain(String message) {
+  boolean webLogsContain(String message) {
     if (orchestrator.getServer() == null) {
       return false;
     }