Merge branch 'stable-3.0' into stable-3.1

* stable-3.0:
  Log websessions when they are produced and consumed

Change-Id: If95735b9fcff9040596e8116fced01eb2c20eaba
diff --git a/src/main/java/com/googlesource/gerrit/plugins/websession/broker/BrokerBasedWebSession.java b/src/main/java/com/googlesource/gerrit/plugins/websession/broker/BrokerBasedWebSession.java
index fbd0882..733581c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/websession/broker/BrokerBasedWebSession.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/websession/broker/BrokerBasedWebSession.java
@@ -27,9 +27,12 @@
 import com.google.gerrit.server.events.EventTypes;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Scopes;
 import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.RequestScoped;
 import com.google.inject.servlet.ServletScopes;
+import com.googlesource.gerrit.plugins.websession.broker.log.Log4jWebSessionLogger;
+import com.googlesource.gerrit.plugins.websession.broker.log.WebSessionLogger;
 import java.lang.annotation.Annotation;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -49,6 +52,9 @@
 
       listener(BrokerBasedWebSessionCache.class);
       listener(BrokerBasedWebSessionCacheCleaner.class);
+      listener(Log4jWebSessionLogger.class);
+
+      bind(WebSessionLogger.class).to(Log4jWebSessionLogger.class).in(Scopes.SINGLETON);
     }
 
     private void listener(Class<? extends LifecycleListener> classObj) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/websession/broker/BrokerBasedWebSessionCache.java b/src/main/java/com/googlesource/gerrit/plugins/websession/broker/BrokerBasedWebSessionCache.java
index 6dc1689..147fa93 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/websession/broker/BrokerBasedWebSessionCache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/websession/broker/BrokerBasedWebSessionCache.java
@@ -31,6 +31,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
+import com.googlesource.gerrit.plugins.websession.broker.log.WebSessionLogger;
+import com.googlesource.gerrit.plugins.websession.broker.log.WebSessionLogger.Direction;
 import com.googlesource.gerrit.plugins.websession.broker.util.TimeMachine;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -40,6 +42,7 @@
 import java.time.Instant;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Optional;
 import java.util.UUID;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentMap;
@@ -56,6 +59,7 @@
   String webSessionTopicName;
   DynamicItem<BrokerApi> brokerApi;
   TimeMachine timeMachine;
+  private final WebSessionLogger webSessionLogger;
 
   @Inject
   public BrokerBasedWebSessionCache(
@@ -63,11 +67,13 @@
       DynamicItem<BrokerApi> brokerApi,
       TimeMachine timeMachine,
       PluginConfigFactory cfg,
-      @PluginName String pluginName) {
+      @PluginName String pluginName,
+      WebSessionLogger webSessionLogger) {
     this.cache = cache;
     this.brokerApi = brokerApi;
     this.timeMachine = timeMachine;
     this.webSessionTopicName = getWebSessionTopicName(cfg, pluginName);
+    this.webSessionLogger = webSessionLogger;
   }
 
   protected void processMessage(EventMessage message) {
@@ -84,6 +90,8 @@
         try (ByteArrayInputStream in = new ByteArrayInputStream(event.payload);
             ObjectInputStream inputStream = new ObjectInputStream(in)) {
           Val value = (Val) inputStream.readObject();
+
+          webSessionLogger.log(Direction.CONSUME, webSessionTopicName, event, Optional.of(value));
           Instant expires = Instant.ofEpochMilli(value.getExpiresAt());
           if (expires.isAfter(timeMachine.now())) {
             cache.put(event.key, value);
@@ -96,6 +104,8 @@
         break;
       case REMOVE:
         cache.invalidate(event.key);
+        webSessionLogger.log(Direction.CONSUME, webSessionTopicName, event, Optional.empty());
+
         break;
       default:
         logger.atWarning().log(
@@ -181,18 +191,19 @@
       objectOutputStream.writeObject(value);
       out.flush();
       byte[] serializedObject = out.toByteArray();
-      EventMessage message =
-          brokerApi
-              .get()
-              .newMessage(UUID.randomUUID(), new WebSessionEvent(key, serializedObject, operation));
+      WebSessionEvent webSessionEvent = new WebSessionEvent(key, serializedObject, operation);
+      EventMessage message = brokerApi.get().newMessage(UUID.randomUUID(), webSessionEvent);
       succeeded = brokerApi.get().send(webSessionTopicName, message);
+      if (succeeded) {
+        webSessionLogger.log(
+            Direction.PUBLISH, webSessionTopicName, webSessionEvent, Optional.ofNullable(value));
+      } else {
+        logger.atSevere().log(
+            "Cannot send web-session message for '%s Topic: '%s'", key, webSessionTopicName);
+      }
     } catch (IOException e) {
       logger.atSevere().withCause(e).log(
           "Cannot serialize event for account id '%s': [Exception: %s]", value.getAccountId());
-    } finally {
-      if (!succeeded)
-        logger.atSevere().log(
-            "Cannot send web-session message for '%s Topic: '%s'", key, webSessionTopicName);
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/websession/broker/log/Log4jWebSessionLogger.java b/src/main/java/com/googlesource/gerrit/plugins/websession/broker/log/Log4jWebSessionLogger.java
new file mode 100644
index 0000000..f10a3bf
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/websession/broker/log/Log4jWebSessionLogger.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.websession.broker.log;
+
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.httpd.WebSessionManager.Val;
+import com.google.gerrit.server.util.PluginLogFile;
+import com.google.gerrit.server.util.SystemLog;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.websession.broker.BrokerBasedWebSessionCache.WebSessionEvent;
+import java.util.Optional;
+import org.apache.log4j.PatternLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Log4jWebSessionLogger extends PluginLogFile implements WebSessionLogger {
+  private static final String LOG_NAME = "websession_log";
+  private final Logger msgLog;
+
+  @Inject
+  public Log4jWebSessionLogger(SystemLog systemLog, ServerInformation serverInfo) {
+    super(systemLog, serverInfo, LOG_NAME, new PatternLayout("[%d{ISO8601}] [%t] %-5p : %m%n"));
+    this.msgLog = LoggerFactory.getLogger(LOG_NAME);
+  }
+
+  @Override
+  public void log(Direction direction, String topic, WebSessionEvent event, Optional<Val> payload) {
+    msgLog.info("{} {} {} {}", direction, topic, formatEvent(event), formatSession(payload));
+  }
+
+  private Object formatEvent(WebSessionEvent event) {
+    return String.format(
+        "{ \"operation\":\"%s\", \"key\":\"%s\", \"eventCreatedOn\":%d }",
+        event.operation, event.key, event.eventCreatedOn);
+  }
+
+  private Object formatSession(Optional<Val> payload) {
+    return payload
+        .map(
+            session ->
+                String.format(
+                    "{ \"accountId\":%d, \"expiresAt\":%d }",
+                    session.getAccountId().get(), session.getExpiresAt()))
+        .orElse("");
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/websession/broker/log/WebSessionLogger.java b/src/main/java/com/googlesource/gerrit/plugins/websession/broker/log/WebSessionLogger.java
new file mode 100644
index 0000000..37b04c2
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/websession/broker/log/WebSessionLogger.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.websession.broker.log;
+
+import com.google.gerrit.httpd.WebSessionManager.Val;
+import com.googlesource.gerrit.plugins.websession.broker.BrokerBasedWebSessionCache.WebSessionEvent;
+import java.util.Optional;
+
+public interface WebSessionLogger {
+
+  public enum Direction {
+    PUBLISH,
+    CONSUME;
+  }
+
+  public void log(Direction direction, String topic, WebSessionEvent event, Optional<Val> payload);
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/websession/broker/BrokerBasedWebSessionCacheTest.java b/src/test/java/com/googlesource/gerrit/plugins/websession/broker/BrokerBasedWebSessionCacheTest.java
index 746d3e6..60e995e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/websession/broker/BrokerBasedWebSessionCacheTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/websession/broker/BrokerBasedWebSessionCacheTest.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.events.Event;
 import com.googlesource.gerrit.plugins.websession.broker.BrokerBasedWebSessionCache.WebSessionEvent;
 import com.googlesource.gerrit.plugins.websession.broker.BrokerBasedWebSessionCache.WebSessionEvent.Operation;
+import com.googlesource.gerrit.plugins.websession.broker.log.WebSessionLogger;
 import com.googlesource.gerrit.plugins.websession.broker.util.TimeMachine;
 import java.time.Instant;
 import java.util.UUID;
@@ -72,6 +73,7 @@
   @Mock TimeMachine timeMachine;
   @Mock PluginConfigFactory cfg;
   @Mock PluginConfig pluginConfig;
+  @Mock WebSessionLogger webSessionLogger;
   @Captor ArgumentCaptor<EventMessage> eventCaptor;
   @Captor ArgumentCaptor<Val> valCaptor;
 
@@ -84,7 +86,9 @@
     when(cfg.getFromGerritConfig(PLUGIN_NAME)).thenReturn(pluginConfig);
     when(timeMachine.now()).thenReturn(Instant.EPOCH);
     DynamicItem<BrokerApi> item = DynamicItem.itemOf(BrokerApi.class, brokerApi);
-    objectUnderTest = new BrokerBasedWebSessionCache(cache, item, timeMachine, cfg, PLUGIN_NAME);
+    objectUnderTest =
+        new BrokerBasedWebSessionCache(
+            cache, item, timeMachine, cfg, PLUGIN_NAME, webSessionLogger);
   }
 
   @Test