CASSANDRASC-34: Allow for LoggerHandler to be injected
Currently, `vertxRouter` adds an instance of `LoggerHandler` to the top level route.
This is prescriptive and it doesn't allow for a different implementation of the LoggerHandler
to be injected.
In this commit, `LoggerHandler` is created in the `MainModule` as a singleton and then
injected in the `vertxRouter` method. This allows for a new implementation of the `LoggerHandler`
to be provided.
diff --git a/CHANGES.txt b/CHANGES.txt
index c72ce7d..27ce525 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
1.0.0
-----
+ * Allow injecting a LoggerHandler to vertxRouter (CASSANDRASC-34)
* Optionally support multiple cassandra instances in Sidecar (CASSANDRASC-33)
* Call the start method of CassandraAdaptorDelegate to start periodic health check (CASSANDRASC-32)
* Avoid having sidecar's health response code depend on Cassandra's health information (CASSANDRASC-29)
diff --git a/gradle.properties b/gradle.properties
index c9d988b..f004357 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
version=1.0-SNAPSHOT
junitVersion=5.4.2
kubernetesClientVersion=9.0.0
-cassandra40Version=4.0.1
+cassandra40Version=4.0.3
vertxVersion=4.2.1
diff --git a/src/main/java/org/apache/cassandra/sidecar/MainModule.java b/src/main/java/org/apache/cassandra/sidecar/MainModule.java
index 32c3548..a7eb33d 100644
--- a/src/main/java/org/apache/cassandra/sidecar/MainModule.java
+++ b/src/main/java/org/apache/cassandra/sidecar/MainModule.java
@@ -129,10 +129,10 @@
@Provides
@Singleton
- public Router vertxRouter(Vertx vertx)
+ public Router vertxRouter(Vertx vertx, LoggerHandler loggerHandler)
{
Router router = Router.router(vertx);
- router.route().handler(LoggerHandler.create());
+ router.route().handler(loggerHandler);
// Static web assets for Swagger
StaticHandler swaggerStatic = StaticHandler.create("META-INF/resources/webjars/swagger-ui");
@@ -254,4 +254,11 @@
{
return SidecarRateLimiter.create(config.getRateLimitStreamRequestsPerSecond());
}
+
+ @Provides
+ @Singleton
+ public LoggerHandler loggerHandler()
+ {
+ return LoggerHandler.create();
+ }
}
diff --git a/src/test/java/org/apache/cassandra/sidecar/LoggerHandlerInjectionTest.java b/src/test/java/org/apache/cassandra/sidecar/LoggerHandlerInjectionTest.java
new file mode 100644
index 0000000..d658ddb
--- /dev/null
+++ b/src/test/java/org/apache/cassandra/sidecar/LoggerHandlerInjectionTest.java
@@ -0,0 +1,147 @@
+package org.apache.cassandra.sidecar;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.slf4j.Logger;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.util.Modules;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpServer;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.client.HttpResponse;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.codec.BodyCodec;
+import io.vertx.ext.web.handler.HttpException;
+import io.vertx.ext.web.handler.LoggerFormatter;
+import io.vertx.ext.web.handler.LoggerHandler;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
+import static io.netty.handler.codec.http.HttpResponseStatus.NO_CONTENT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("LoggerHandler Injection Test")
+@ExtendWith(VertxExtension.class)
+public class LoggerHandlerInjectionTest
+{
+ private Vertx vertx;
+ private Configuration config;
+ private final Logger logger = mock(Logger.class);
+
+ @BeforeEach
+ void setUp() throws InterruptedException
+ {
+ FakeLoggerHandler loggerHandler = new FakeLoggerHandler(logger);
+ Injector injector = Guice.createInjector(Modules.override(Modules.override(new MainModule()).with(new TestModule()))
+ .with(binder -> binder.bind(LoggerHandler.class).toInstance(loggerHandler)));
+ vertx = injector.getInstance(Vertx.class);
+ config = injector.getInstance(Configuration.class);
+ Router router = injector.getInstance(Router.class);
+
+ router.get("/500-route").handler(p -> {
+ throw new RuntimeException("Fails with 500");
+ });
+
+ router.get("/404-route").handler(p -> {
+ throw new HttpException(NOT_FOUND.code(), "Sorry, it's not here");
+ });
+
+ router.get("/204-route").handler(p -> {
+ throw new HttpException(NO_CONTENT.code(), "Sorry, no content");
+ });
+
+ VertxTestContext context = new VertxTestContext();
+ HttpServer server = injector.getInstance(HttpServer.class);
+ server.listen(config.getPort(), context.succeedingThenComplete());
+
+ context.awaitCompletion(5, TimeUnit.SECONDS);
+ }
+
+ @DisplayName("Should log at error level when the request fails with a 500 code")
+ @Test
+ public void testInjectedLoggerHandlerLogsAtErrorLevel(VertxTestContext testContext)
+ {
+ helper("/500-route", testContext, 500, "Internal Server Error");
+ }
+
+ @DisplayName("Should log at warn level when the request fails with a 404 error")
+ @Test
+ public void testInjectedLoggerHandlerLogsAtWarnLevel(VertxTestContext testContext)
+ {
+ helper("/404-route", testContext, 404, "Not Found");
+ }
+
+ @DisplayName("Should log at info level when the request returns with a 500 error")
+ @Test
+ public void testInjectedLoggerHandlerLogsAtInfoLevel(VertxTestContext testContext)
+ {
+ helper("/204-route", testContext, 204, null);
+ }
+
+ private void helper(String requestURI, VertxTestContext testContext, int expectedStatusCode, String expectedBody)
+ {
+ WebClient client = WebClient.create(vertx);
+ Handler<HttpResponse<String>> responseVerifier = response -> testContext.verify(
+ () -> {
+ assertThat(response.statusCode()).isEqualTo(expectedStatusCode);
+ if (expectedBody == null)
+ {
+ assertThat(response.body()).isNull();
+ }
+ else
+ {
+ assertThat(response.body()).isEqualTo(expectedBody);
+ }
+ testContext.completeNow();
+ verify(logger, times(1)).info("{}", expectedStatusCode);
+ });
+ client.get(config.getPort(), "localhost", requestURI)
+ .as(BodyCodec.string())
+ .ssl(false)
+ .send(testContext.succeeding(responseVerifier));
+ }
+
+ private static class FakeLoggerHandler implements LoggerHandler
+ {
+ private final Logger logger;
+
+ FakeLoggerHandler(Logger logger)
+ {
+ this.logger = logger;
+ }
+
+ @Override
+ public LoggerHandler customFormatter(Function<HttpServerRequest, String> formatter)
+ {
+ return this;
+ }
+
+ @Override
+ public LoggerHandler customFormatter(LoggerFormatter formatter)
+ {
+ return this;
+ }
+
+ @Override
+ public void handle(RoutingContext context)
+ {
+ HttpServerRequest request = context.request();
+ context.addBodyEndHandler(v -> logger.info("{}", request.response().getStatusCode()));
+ context.next();
+ }
+ }
+}