diff --git a/loki-log4j2-appender/pom.xml b/loki-log4j2-appender/pom.xml index 47d2614..92a91ad 100644 --- a/loki-log4j2-appender/pom.xml +++ b/loki-log4j2-appender/pom.xml @@ -55,6 +55,13 @@ test + + io.dropwizard.metrics + metrics-core + 4.1.17 + provided + + org.junit.jupiter diff --git a/loki-log4j2-appender/src/main/java/pl/tkowalcz/tjahzi/log4j2/LokiAppender.java b/loki-log4j2-appender/src/main/java/pl/tkowalcz/tjahzi/log4j2/LokiAppender.java index 625e136..991a274 100644 --- a/loki-log4j2-appender/src/main/java/pl/tkowalcz/tjahzi/log4j2/LokiAppender.java +++ b/loki-log4j2-appender/src/main/java/pl/tkowalcz/tjahzi/log4j2/LokiAppender.java @@ -68,6 +68,10 @@ public class LokiAppender extends AbstractAppender { return loggingSystem; } + /** + * This is an entry point to set monitoring (statistics) hooks for this appender. This + * API is in beta and is subject to change (and probably will). + */ public void setMonitoringModule(MonitoringModule monitoringModule) { monitoringModuleWrapper.setMonitoringModule(monitoringModule); } diff --git a/loki-log4j2-appender/src/test/java/pl/tkowalcz/tjahzi/log4j2/LokiAppenderMonitoringTest.java b/loki-log4j2-appender/src/test/java/pl/tkowalcz/tjahzi/log4j2/LokiAppenderMonitoringTest.java new file mode 100644 index 0000000..abd007d --- /dev/null +++ b/loki-log4j2-appender/src/test/java/pl/tkowalcz/tjahzi/log4j2/LokiAppenderMonitoringTest.java @@ -0,0 +1,58 @@ +package pl.tkowalcz.tjahzi.log4j2; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.MetricRegistry; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Testcontainers; +import pl.tkowalcz.tjahzi.stats.DropwizardMonitoringModule; + +import java.net.URI; +import java.net.URISyntaxException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@Testcontainers +class LokiAppenderMonitoringTest { + + @Test + void shouldInjectMonitoringAndUseIt() throws URISyntaxException { + // Given + URI uri = getClass() + .getClassLoader() + .getResource("basic-appender-test-log4j2-configuration.xml") + .toURI(); + + LoggerContext context = (LoggerContext) LogManager.getContext(false); + context.setConfigLocation(uri); + + LokiAppender loki = context.getConfiguration().getAppender("Loki"); + MetricRegistry metricRegistry = new MetricRegistry(); + loki.setMonitoringModule( + new DropwizardMonitoringModule( + metricRegistry, + "appender.loki" + ) + ); + + // When + Logger logger = LogManager.getLogger(LokiAppenderMonitoringTest.class); + logger.info("Test test test"); + + // Then + assertThat(metricRegistry.getMetrics()).isNotEmpty(); + + Counter connectAttemptsCounter = metricRegistry + .getCounters() + .get("appender.loki.httpConnectAttempts"); + assertThat(connectAttemptsCounter).isNotNull(); + + await().untilAsserted(() -> { + assertThat(connectAttemptsCounter.getCount()).isEqualTo(1); + } + ); + } +} diff --git a/tjahzi-core/pom.xml b/tjahzi-core/pom.xml index 9da063c..879a39e 100644 --- a/tjahzi-core/pom.xml +++ b/tjahzi-core/pom.xml @@ -76,9 +76,9 @@ - io.dropwizard - dropwizard-core - 2.0.18 + io.dropwizard.metrics + metrics-core + 4.1.17 provided diff --git a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/HttpClientInitializer.java b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/HttpClientInitializer.java index 6d33a99..3cb928e 100644 --- a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/HttpClientInitializer.java +++ b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/HttpClientInitializer.java @@ -1,21 +1,21 @@ package pl.tkowalcz.tjahzi.http; +import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; -import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.timeout.IdleStateHandler; import pl.tkowalcz.tjahzi.stats.MonitoringModule; -class HttpClientInitializer extends ChannelInitializer { +class HttpClientInitializer extends ChannelInitializer { private static final int BYTES_IN_MEGABYTE = 1024 * 1024; private static final int MAX_CONTENT_LENGTH = 10 * BYTES_IN_MEGABYTE; private final MonitoringModule monitoringModule; - private final ResponseHandler responseHandler; + private final RequestAndResponseHandler responseHandler; private final int requestTimeoutMillis; private final int maxRequestsInFlight; @@ -26,14 +26,14 @@ class HttpClientInitializer extends ChannelInitializer { int maxRequestsInFlight ) { this.monitoringModule = monitoringModule; - this.responseHandler = new ResponseHandler(monitoringModule); + this.responseHandler = new RequestAndResponseHandler(monitoringModule); this.requestTimeoutMillis = requestTimeoutMillis; this.maxRequestsInFlight = maxRequestsInFlight; } @Override - protected void initChannel(SocketChannel ch) { + protected void initChannel(Channel ch) { ChannelPipeline p = ch.pipeline(); p.addLast(new IdleStateHandler( requestTimeoutMillis, @@ -50,7 +50,6 @@ class HttpClientInitializer extends ChannelInitializer { maxRequestsInFlight ) ); - p.addLast(new InFlightRequestsTracker()); p.addLast(responseHandler); } } diff --git a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/HttpConnection.java b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/HttpConnection.java index 76bfdd5..ff4ef80 100644 --- a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/HttpConnection.java +++ b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/HttpConnection.java @@ -68,7 +68,6 @@ public class HttpConnection implements Closeable { stableReference.awaitUninterruptibly(); if (stableReference.isSuccess() && stableReference.channel().isActive()) { stableReference.channel().writeAndFlush(request); - monitoringModule.incrementSentHttpRequests(request.content().readableBytes()); } else { retry.retry(); } diff --git a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/InFlightRequestsTracker.java b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/InFlightRequestsTracker.java deleted file mode 100644 index c75db9b..0000000 --- a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/InFlightRequestsTracker.java +++ /dev/null @@ -1,24 +0,0 @@ -package pl.tkowalcz.tjahzi.http; - -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; - -import java.util.concurrent.atomic.AtomicInteger; - -public class InFlightRequestsTracker extends ChannelDuplexHandler { - - private final AtomicInteger requestsInFlight = new AtomicInteger(); - - @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - super.write(ctx, msg, promise); - requestsInFlight.incrementAndGet(); - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - super.channelRead(ctx, msg); - requestsInFlight.decrementAndGet(); - } -} diff --git a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/PipelinedHttpRequestTimer.java b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/PipelinedHttpRequestTimer.java index 6945c70..d294cd7 100644 --- a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/PipelinedHttpRequestTimer.java +++ b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/PipelinedHttpRequestTimer.java @@ -31,3 +31,4 @@ public class PipelinedHttpRequestTimer extends ChannelDuplexHandler { monitoringModule.recordResponseTime(timer.measure()); } } + diff --git a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/ResponseHandler.java b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/RequestAndResponseHandler.java similarity index 50% rename from tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/ResponseHandler.java rename to tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/RequestAndResponseHandler.java index c6efc4c..ab1e7f1 100644 --- a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/ResponseHandler.java +++ b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/http/RequestAndResponseHandler.java @@ -1,28 +1,46 @@ package pl.tkowalcz.tjahzi.http; +import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpStatusClass; +import io.netty.handler.codec.http.HttpUtil; import pl.tkowalcz.tjahzi.stats.MonitoringModule; @ChannelHandler.Sharable -class ResponseHandler extends SimpleChannelInboundHandler { +class RequestAndResponseHandler extends ChannelDuplexHandler { private final MonitoringModule monitoringModule; - ResponseHandler(MonitoringModule monitoringModule) { - super(false); + RequestAndResponseHandler(MonitoringModule monitoringModule) { this.monitoringModule = monitoringModule; } @Override - protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) { + public void write(ChannelHandlerContext ctx, Object object, ChannelPromise promise) throws Exception { + if (object instanceof FullHttpRequest) { + int payloadSize = ((FullHttpRequest) object).content().readableBytes(); + monitoringModule.incrementSentHttpRequests(payloadSize); + } + + super.write(ctx, object, promise); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object object) { + FullHttpResponse msg = (FullHttpResponse) object; + monitoringModule.incrementHttpResponses(); if (msg.status().codeClass() != HttpStatusClass.SUCCESS) { monitoringModule.incrementHttpErrors(msg.status(), msg.content()); } + + if (!HttpUtil.isKeepAlive(msg)) { + ctx.close(); + } } @Override diff --git a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/stats/StandardMonitoringModule.java b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/stats/StandardMonitoringModule.java index 3fbff74..efaacca 100644 --- a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/stats/StandardMonitoringModule.java +++ b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/stats/StandardMonitoringModule.java @@ -58,16 +58,32 @@ public class StandardMonitoringModule implements MonitoringModule { sentBytes.addAndGet(sizeBytes); } + public long getSentHttpRequests() { + return sentHttpRequests.get(); + } + + public long getSentBytes() { + return sentBytes.get(); + } + @Override public void incrementFailedHttpRequests() { failedHttpRequests.incrementAndGet(); } + public long getFailedHttpRequests() { + return failedHttpRequests.get(); + } + @Override public void incrementRetriedHttpRequests() { retriedHttpRequests.incrementAndGet(); } + public long getRetriedHttpRequests() { + return retriedHttpRequests.get(); + } + @Override public void addAgentError(Throwable throwable) { agentErrors.incrementAndGet(); @@ -79,6 +95,10 @@ public class StandardMonitoringModule implements MonitoringModule { httpConnectAttempts.incrementAndGet(); } + public long getHttpConnectAttempts() { + return httpConnectAttempts.get(); + } + @Override public void incrementChannelInactive() { channelInactive.incrementAndGet(); @@ -89,6 +109,10 @@ public class StandardMonitoringModule implements MonitoringModule { httpResponses.incrementAndGet(); } + public long getHttpResponses() { + return httpResponses.get(); + } + @Override public void addPipelineError(Throwable cause) { distinctErrorLog.record(cause); @@ -96,6 +120,7 @@ public class StandardMonitoringModule implements MonitoringModule { @Override public void incrementHttpErrors(HttpResponseStatus status, ByteBuf content) { + failedHttpRequests.incrementAndGet(); distinctErrorLog.record( new RuntimeException( content.toString(Charset.defaultCharset()) diff --git a/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/http/BlockingRetryTest.java b/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/http/BlockingRetryTest.java index 0347d80..1c4d672 100644 --- a/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/http/BlockingRetryTest.java +++ b/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/http/BlockingRetryTest.java @@ -6,16 +6,14 @@ import pl.tkowalcz.tjahzi.stats.StandardMonitoringModule; import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; class BlockingRetryTest { - private StandardMonitoringModule monitoringModule = mock(StandardMonitoringModule.class); - @Test void shouldRetryAndInvokeOperation() { // Given AtomicInteger retryCounter = new AtomicInteger(); + StandardMonitoringModule monitoringModule = new StandardMonitoringModule(); BlockingRetry retry = new BlockingRetry( __ -> { @@ -32,12 +30,15 @@ class BlockingRetryTest { // Then assertThat(retryCounter).hasValue(5); + assertThat(monitoringModule.getRetriedHttpRequests()).isEqualTo(5); + assertThat(monitoringModule.getFailedHttpRequests()).isEqualTo(1); } @Test void shouldNotRetryOnItsOwn() { // Given AtomicInteger retryCounter = new AtomicInteger(); + StandardMonitoringModule monitoringModule = new StandardMonitoringModule(); BlockingRetry retry = new BlockingRetry( __ -> { @@ -53,5 +54,7 @@ class BlockingRetryTest { // Then assertThat(retryCounter).hasValue(1); + assertThat(monitoringModule.getRetriedHttpRequests()).isEqualTo(1); + assertThat(monitoringModule.getFailedHttpRequests()).isEqualTo(0); } } diff --git a/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/http/EventLoopGroupRetryTest.java b/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/http/EventLoopGroupRetryTest.java index 68b5b66..49e1e5c 100644 --- a/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/http/EventLoopGroupRetryTest.java +++ b/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/http/EventLoopGroupRetryTest.java @@ -12,17 +12,16 @@ import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -import static org.mockito.Mockito.mock; class EventLoopGroupRetryTest { - private final StandardMonitoringModule monitoringModule = mock(StandardMonitoringModule.class); - private NioEventLoopGroup eventLoopGroup; + private StandardMonitoringModule monitoringModule; @BeforeEach void setUp() { eventLoopGroup = new NioEventLoopGroup(Executors.defaultThreadFactory()); + monitoringModule = new StandardMonitoringModule(); } @AfterEach @@ -34,6 +33,7 @@ class EventLoopGroupRetryTest { void shouldRetryAndInvokeOperation() { // Given AtomicInteger retryCounter = new AtomicInteger(); + StandardMonitoringModule monitoringModule = new StandardMonitoringModule(); EventLoopGroupRetry retry = new EventLoopGroupRetry( eventLoopGroup, @@ -52,6 +52,8 @@ class EventLoopGroupRetryTest { await().atMost(Durations.TEN_SECONDS).untilAsserted(() -> { assertThat(retryCounter).hasValueGreaterThan(3); }); + + assertThat(monitoringModule.getHttpConnectAttempts()).isGreaterThanOrEqualTo(3); } @Test @@ -75,5 +77,7 @@ class EventLoopGroupRetryTest { await().atMost(Durations.TEN_SECONDS).untilAsserted(() -> { assertThat(retryCounter).hasValue(1); }); + + assertThat(monitoringModule.getHttpConnectAttempts()).isEqualTo(1); } } diff --git a/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/http/PipelinedHttpRequestTimerTest.java b/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/http/PipelinedHttpRequestTimerTest.java new file mode 100644 index 0000000..ad1d5d8 --- /dev/null +++ b/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/http/PipelinedHttpRequestTimerTest.java @@ -0,0 +1,228 @@ +package pl.tkowalcz.tjahzi.http; + +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.*; +import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.org.apache.commons.lang.mutable.MutableLong; +import pl.tkowalcz.tjahzi.stats.SettableClock; +import pl.tkowalcz.tjahzi.stats.StandardMonitoringModule; + +import java.time.Clock; + +import static org.assertj.core.api.Assertions.assertThat; + +class PipelinedHttpRequestTimerTest { + + @Test + void shouldCloseChannelIfNoKeepalive() { + // Given + SettableClock clock = new SettableClock(); + + StandardMonitoringModule monitoringModule = new StandardMonitoringModule() { + @Override + public Clock getClock() { + return clock; + } + }; + + HttpClientInitializer initializer = new HttpClientInitializer( + monitoringModule, + 10_000, + 42 + ); + + FullHttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, + HttpMethod.POST, + ClientConfigurationBuilder.DEFAULT_LOG_ENDPOINT, + Unpooled.wrappedBuffer("Boo hoo".getBytes()) + ); + + HttpHeaders headers = new DefaultHttpHeaders(); + headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + + DefaultFullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.NO_CONTENT, + Unpooled.wrappedBuffer("OK".getBytes()), + headers, + EmptyHttpHeaders.INSTANCE + ); + + // When + EmbeddedChannel channel = new EmbeddedChannel(initializer); + channel.writeOneOutbound(request); + channel.flush(); + + channel.writeOneInbound(response); + channel.flush(); + + // Then + assertThat(channel.isOpen()).isFalse(); + } + + @Test + void shouldNotCloseChannelOnKeepalive() { + // Given + SettableClock clock = new SettableClock(); + + StandardMonitoringModule monitoringModule = new StandardMonitoringModule() { + @Override + public Clock getClock() { + return clock; + } + }; + + HttpClientInitializer initializer = new HttpClientInitializer( + monitoringModule, + 10_000, + 42 + ); + + FullHttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, + HttpMethod.POST, + ClientConfigurationBuilder.DEFAULT_LOG_ENDPOINT, + Unpooled.wrappedBuffer("Boo hoo".getBytes()) + ); + + HttpHeaders headers = new DefaultHttpHeaders(); + headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + + DefaultFullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.NO_CONTENT, + Unpooled.wrappedBuffer("OK".getBytes()), + headers, + EmptyHttpHeaders.INSTANCE + ); + + // When + EmbeddedChannel channel = new EmbeddedChannel(initializer); + channel.writeOneOutbound(request); + channel.flush(); + + channel.writeOneInbound(response); + channel.flush(); + + // Then + assertThat(channel.isOpen()).isTrue(); + } + + @Test + void shouldReportRequestMetrics() { + // Given + SettableClock clock = new SettableClock(); + MutableLong requestRTT = new MutableLong(); + + StandardMonitoringModule monitoringModule = new StandardMonitoringModule() { + @Override + public Clock getClock() { + return clock; + } + + @Override + public void recordResponseTime(long time) { + requestRTT.setValue(time); + } + }; + + HttpClientInitializer initializer = new HttpClientInitializer( + monitoringModule, + 10_000, + 42 + ); + + FullHttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, + HttpMethod.POST, + ClientConfigurationBuilder.DEFAULT_LOG_ENDPOINT, + Unpooled.wrappedBuffer("Boo hoo".getBytes()) + ); + + HttpHeaders headers = new DefaultHttpHeaders(); + headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + + DefaultFullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.NO_CONTENT + ); + + // When + EmbeddedChannel channel = new EmbeddedChannel(initializer); + channel.writeOneOutbound(request); + channel.flush(); + + clock.tick(); + + channel.writeOneInbound(response); + channel.flush(); + + // Then + assertThat(monitoringModule.getFailedHttpRequests()).isZero(); + assertThat(monitoringModule.getSentHttpRequests()).isEqualTo(1); + assertThat(monitoringModule.getHttpResponses()).isEqualTo(1); + assertThat(monitoringModule.getSentBytes()).isEqualTo(7); + + assertThat(requestRTT.intValue()).isEqualTo(SettableClock.TICK_AMOUNT); + } + + @Test + void shouldReportRequestMetricsOnError() { + // Given + SettableClock clock = new SettableClock(); + MutableLong requestRTT = new MutableLong(); + + StandardMonitoringModule monitoringModule = new StandardMonitoringModule() { + @Override + public Clock getClock() { + return clock; + } + + @Override + public void recordResponseTime(long time) { + requestRTT.setValue(time); + } + }; + + HttpClientInitializer initializer = new HttpClientInitializer( + monitoringModule, + 10_000, + 42 + ); + + FullHttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, + HttpMethod.POST, + ClientConfigurationBuilder.DEFAULT_LOG_ENDPOINT, + Unpooled.wrappedBuffer("Boo hoo".getBytes()) + ); + + HttpHeaders headers = new DefaultHttpHeaders(); + headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + + DefaultFullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.INTERNAL_SERVER_ERROR + ); + + // When + EmbeddedChannel channel = new EmbeddedChannel(initializer); + channel.writeOneOutbound(request); + channel.flush(); + + clock.tick(); + + channel.writeOneInbound(response); + channel.flush(); + + // Then + assertThat(monitoringModule.getFailedHttpRequests()).isEqualTo(1); + assertThat(monitoringModule.getSentHttpRequests()).isEqualTo(1); + assertThat(monitoringModule.getHttpResponses()).isEqualTo(1); + assertThat(monitoringModule.getSentBytes()).isEqualTo(7); + + assertThat(requestRTT.intValue()).isEqualTo(SettableClock.TICK_AMOUNT); + } +} diff --git a/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/stats/SettableClock.java b/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/stats/SettableClock.java new file mode 100644 index 0000000..c5d7c22 --- /dev/null +++ b/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/stats/SettableClock.java @@ -0,0 +1,44 @@ +package pl.tkowalcz.tjahzi.stats; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; + +public class SettableClock extends Clock { + + public static final long TICK_AMOUNT = 11L; + + private long millis; + + public void setMillis(long millis) { + this.millis = millis; + } + + public void tick() { + millis += TICK_AMOUNT; + } + + public void tick(long amount) { + millis += amount; + } + + @Override + public ZoneId getZone() { + return ZoneId.systemDefault(); + } + + @Override + public Clock withZone(ZoneId zone) { + throw new UnsupportedOperationException(); + } + + @Override + public Instant instant() { + throw new UnsupportedOperationException(); + } + + @Override + public long millis() { + return millis; + } +} diff --git a/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/stats/TimingRingBufferTest.java b/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/stats/TimingRingBufferTest.java new file mode 100644 index 0000000..6fb36c9 --- /dev/null +++ b/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/stats/TimingRingBufferTest.java @@ -0,0 +1,120 @@ +package pl.tkowalcz.tjahzi.stats; + +import org.junit.jupiter.api.Test; + +import java.time.Clock; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TimingRingBufferTest { + + @Test + void shouldActLikeARingBufferOfTimings() { + // Given + Clock clock = mock(Clock.class); + + long firstRecording = 10L; + long secondRecording = 20L; + long thirdRecording = 33L; + long firstResponse = 38L; + long secondResponse = 41L; + long thirdResponse = 56L; + + when(clock.millis()) + .thenReturn(firstRecording) + .thenReturn(secondRecording) + .thenReturn(thirdRecording) + .thenReturn(firstResponse) + .thenReturn(secondResponse) + .thenReturn(thirdResponse); + + TimingRingBuffer buffer = new TimingRingBuffer( + clock, + 10 + ); + + // When + buffer.record(); + buffer.record(); + buffer.record(); + + long firstRTT = buffer.measure(); + long secondRTT = buffer.measure(); + long thirdRTT = buffer.measure(); + + // Then + assertThat(firstRTT).isEqualTo(firstResponse - firstRecording); + assertThat(secondRTT).isEqualTo(secondResponse - secondRecording); + assertThat(thirdRTT).isEqualTo(thirdResponse - thirdRecording); + } + + @Test + void shouldHandleIntermixedResponsesAndNewRexuests() { + // Given + Clock clock = mock(Clock.class); + + long firstRecording = 10L; + long secondRecording = 20L; + long firstResponse = 22L; + long thirdRecording = 33L; + long secondResponse = 41L; + long fourthRecording = 48L; + long thirdResponse = 56L; + long fourthResponse = 58L; + + when(clock.millis()) + .thenReturn(firstRecording) + .thenReturn(secondRecording) + .thenReturn(firstResponse) + .thenReturn(thirdRecording) + .thenReturn(secondResponse) + .thenReturn(fourthRecording) + .thenReturn(thirdResponse) + .thenReturn(fourthResponse); + + TimingRingBuffer buffer = new TimingRingBuffer( + clock, + 10 + ); + + // When + buffer.record(); + buffer.record(); + long firstRTT = buffer.measure(); + buffer.record(); + long secondRTT = buffer.measure(); + buffer.record(); + long thirdRTT = buffer.measure(); + long fourthRTT = buffer.measure(); + + // Then + assertThat(firstRTT).isEqualTo(firstResponse - firstRecording); + assertThat(secondRTT).isEqualTo(secondResponse - secondRecording); + assertThat(thirdRTT).isEqualTo(thirdResponse - thirdRecording); + assertThat(fourthRTT).isEqualTo(fourthResponse - fourthRecording); + } + + @Test + void shouldHandleWrappingAround() { + // Given + SettableClock clock = new SettableClock(); + + TimingRingBuffer buffer = new TimingRingBuffer( + clock, + 10 + ); + + // When & Then + for (int i = 0; i < 23; i++) { + long tick = 40L; + + buffer.record(); + clock.tick(tick); + long actual = buffer.measure(); + + assertThat(actual).isEqualTo(tick); + } + } +}