diff --git a/components/client/src/main/java/com/hotels/styx/client/Connection.java b/components/client/src/main/java/com/hotels/styx/client/Connection.java index 71a1f82ce8..81db790a77 100644 --- a/components/client/src/main/java/com/hotels/styx/client/Connection.java +++ b/components/client/src/main/java/com/hotels/styx/client/Connection.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -65,13 +65,6 @@ interface Factory { */ Origin getOrigin(); - /** - * Returns time to first byte in milliseconds. - * - * @return time to first byte in milliseconds - */ - long getTimeToFirstByteMillis(); - /** * Register a listener connection state events. * diff --git a/components/client/src/main/java/com/hotels/styx/client/applications/AggregateTimer.java b/components/client/src/main/java/com/hotels/styx/client/applications/AggregateTimer.java index 147768faa7..05494daf41 100644 --- a/components/client/src/main/java/com/hotels/styx/client/applications/AggregateTimer.java +++ b/components/client/src/main/java/com/hotels/styx/client/applications/AggregateTimer.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -48,21 +48,21 @@ public void stopAndRecord() { } } - private final Timer requestLatency; - private final Timer applicationLatency; + private final Timer requestTimer; + private final Timer applicationTimer; /** * Constructor. * - * @param requestLatency a timer - * @param applicationLatency a timer + * @param requestTimer a timer + * @param applicationTimer a timer */ - public AggregateTimer(Timer requestLatency, Timer applicationLatency) { - this.requestLatency = requestLatency; - this.applicationLatency = applicationLatency; + public AggregateTimer(Timer requestTimer, Timer applicationTimer) { + this.requestTimer = requestTimer; + this.applicationTimer = applicationTimer; } public Stopper time() { - return new Stopper(requestLatency.time(), applicationLatency.time()); + return new Stopper(requestTimer.time(), applicationTimer.time()); } } diff --git a/components/client/src/main/java/com/hotels/styx/client/applications/OriginStats.java b/components/client/src/main/java/com/hotels/styx/client/applications/OriginStats.java index 8e5323340a..4c98ffeb32 100644 --- a/components/client/src/main/java/com/hotels/styx/client/applications/OriginStats.java +++ b/components/client/src/main/java/com/hotels/styx/client/applications/OriginStats.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -33,10 +33,15 @@ public interface OriginStats { void requestError(); /** - * Starts request latency timer. + * Returns a request latency timer. */ AggregateTimer requestLatencyTimer(); + /** + * Returns a time-to-first-byte timer. + */ + AggregateTimer timeToFirstByteTimer(); + /** * records a response with a status code. * diff --git a/components/client/src/main/java/com/hotels/styx/client/applications/metrics/ApplicationMetrics.java b/components/client/src/main/java/com/hotels/styx/client/applications/metrics/ApplicationMetrics.java index be13db5ea1..d4983124ef 100644 --- a/components/client/src/main/java/com/hotels/styx/client/applications/metrics/ApplicationMetrics.java +++ b/components/client/src/main/java/com/hotels/styx/client/applications/metrics/ApplicationMetrics.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ public class ApplicationMetrics { private final MetricRegistry applicationMetrics; private final MetricRegistry requestScope; private final Timer requestLatencyTimer; + private final Timer requestTimeToFirstByteTimer; private final Meter requestSuccessMeter; private final Meter requestErrorMeter; private final Meter status200OkMeter; @@ -52,6 +53,7 @@ public ApplicationMetrics(Id appId, MetricRegistry metricRegistry) { this.requestScope = this.applicationMetrics.scope("requests"); this.requestLatencyTimer = this.requestScope.timer("latency"); + this.requestTimeToFirstByteTimer = this.requestScope.timer("time-to-first-byte"); this.requestSuccessMeter = this.requestScope.meter("success-rate"); this.requestErrorMeter = this.requestScope.meter("error-rate"); this.status200OkMeter = this.requestScope.meter(name("response", statusCodeName(200))); @@ -73,14 +75,19 @@ public void requestError() { } /** - * Starts request latency timer. - * - * @return timer context for request latency + * Returns request latency timer. */ public Timer requestLatencyTimer() { return requestLatencyTimer; } + /** + * Returns request time-to-first-byte timer. + */ + public Timer requestTimeToFirstByteTimer() { + return requestTimeToFirstByteTimer; + } + /** * Returns the metrics registry used. * diff --git a/components/client/src/main/java/com/hotels/styx/client/applications/metrics/OriginMetrics.java b/components/client/src/main/java/com/hotels/styx/client/applications/metrics/OriginMetrics.java index 29d6d3bdbc..8ab90b7e0e 100644 --- a/components/client/src/main/java/com/hotels/styx/client/applications/metrics/OriginMetrics.java +++ b/components/client/src/main/java/com/hotels/styx/client/applications/metrics/OriginMetrics.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -50,6 +50,7 @@ public class OriginMetrics implements OriginStats { private final Meter requestSuccessMeter; private final Meter requestErrorMeter; private final Timer requestLatency; + private final Timer timeToFirstByte; private final Meter status200OkMeter; private final Meter errorsCatchAll; @@ -60,7 +61,7 @@ public class OriginMetrics implements OriginStats { * Constructor. * * @param applicationMetrics application metrics - * @param origin an origin + * @param originId an origin */ public OriginMetrics(ApplicationMetrics applicationMetrics, String originId) { this.applicationMetrics = requireNonNull(applicationMetrics); @@ -72,6 +73,7 @@ public OriginMetrics(ApplicationMetrics applicationMetrics, String originId) { this.requestSuccessMeter = this.registry.meter(name(this.requestMetricPrefix, "success-rate")); this.requestErrorMeter = this.registry.meter(name(this.requestMetricPrefix, "error-rate")); this.requestLatency = this.registry.timer(name(this.requestMetricPrefix, "latency")); + this.timeToFirstByte = this.registry.timer(name(this.requestMetricPrefix, "time-to-first-byte")); this.status200OkMeter = this.registry.meter(name(this.requestMetricPrefix, "response", statusCodeName(200))); this.errorsCatchAll = this.registry.meter(name(this.requestMetricPrefix, "response.status.5xx")); @@ -81,7 +83,8 @@ public OriginMetrics(ApplicationMetrics applicationMetrics, String originId) { /** * Create a new OriginMetrics. * - * @param origin an origin + * @param appId application ID + * @param originId an origin * @param metricRegistry a metrics registry * @return a new OriginMetrics */ @@ -134,6 +137,11 @@ public AggregateTimer requestLatencyTimer() { return new AggregateTimer(requestLatency, applicationMetrics.requestLatencyTimer()); } + @Override + public AggregateTimer timeToFirstByteTimer() { + return new AggregateTimer(timeToFirstByte, applicationMetrics.requestTimeToFirstByteTimer()); + } + private int httpStatusCodeClass(int code) { if (code < 100 || code >= 600) { return 0; diff --git a/components/client/src/main/java/com/hotels/styx/client/connectionpool/ConnectionPool.java b/components/client/src/main/java/com/hotels/styx/client/connectionpool/ConnectionPool.java index a5716c8788..a8cfd39905 100644 --- a/components/client/src/main/java/com/hotels/styx/client/connectionpool/ConnectionPool.java +++ b/components/client/src/main/java/com/hotels/styx/client/connectionpool/ConnectionPool.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/client/src/main/java/com/hotels/styx/client/connectionpool/ExpiringConnection.java b/components/client/src/main/java/com/hotels/styx/client/connectionpool/ExpiringConnection.java index 5399a195ab..d102ede937 100644 --- a/components/client/src/main/java/com/hotels/styx/client/connectionpool/ExpiringConnection.java +++ b/components/client/src/main/java/com/hotels/styx/client/connectionpool/ExpiringConnection.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -62,11 +62,6 @@ public Origin getOrigin() { return nettyConnection.getOrigin(); } - @Override - public long getTimeToFirstByteMillis() { - return nettyConnection.getTimeToFirstByteMillis(); - } - @Override public void addConnectionListener(Listener listener) { nettyConnection.addConnectionListener(listener); diff --git a/components/client/src/main/java/com/hotels/styx/client/connectionpool/SimpleConnectionPool.java b/components/client/src/main/java/com/hotels/styx/client/connectionpool/SimpleConnectionPool.java index a756202886..951ec5c543 100644 --- a/components/client/src/main/java/com/hotels/styx/client/connectionpool/SimpleConnectionPool.java +++ b/components/client/src/main/java/com/hotels/styx/client/connectionpool/SimpleConnectionPool.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -223,6 +223,7 @@ public ConnectionPool.Stats stats() { @VisibleForTesting private class ConnectionPoolStats implements Stats { + @Override public int availableConnectionCount() { return availableConnections.size(); @@ -263,7 +264,6 @@ public int connectionsInEstablishment() { return connectionsInEstablishment.get(); } - @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/components/client/src/main/java/com/hotels/styx/client/connectionpool/StatsReportingConnectionPool.java b/components/client/src/main/java/com/hotels/styx/client/connectionpool/StatsReportingConnectionPool.java index 2b2fc6c7ea..388c8ae755 100644 --- a/components/client/src/main/java/com/hotels/styx/client/connectionpool/StatsReportingConnectionPool.java +++ b/components/client/src/main/java/com/hotels/styx/client/connectionpool/StatsReportingConnectionPool.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/client/src/main/java/com/hotels/styx/client/connectionpool/stubs/StubConnectionFactory.java b/components/client/src/main/java/com/hotels/styx/client/connectionpool/stubs/StubConnectionFactory.java index 2ebb1fcd7d..976dc5c7ce 100644 --- a/components/client/src/main/java/com/hotels/styx/client/connectionpool/stubs/StubConnectionFactory.java +++ b/components/client/src/main/java/com/hotels/styx/client/connectionpool/stubs/StubConnectionFactory.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -64,11 +64,6 @@ public Origin getOrigin() { return this.origin; } - @Override - public long getTimeToFirstByteMillis() { - return 0; - } - @Override public void addConnectionListener(Listener listener) { listeners.addListener(listener); diff --git a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyConnection.java b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyConnection.java index 52ba1179ae..2d6a89a70e 100644 --- a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyConnection.java +++ b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyConnection.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32,7 +32,6 @@ import reactor.core.publisher.Flux; import java.util.Optional; -import java.util.concurrent.TimeUnit; import static com.google.common.base.Objects.toStringHelper; import static java.util.Objects.requireNonNull; @@ -40,7 +39,7 @@ /** * A connection using a netty channel. */ -public class NettyConnection implements Connection, TimeToFirstByteListener { +public class NettyConnection implements Connection { private static final AttributeKey CLOSED_BY_STYX = AttributeKey.newInstance("CLOSED_BY_STYX"); private static final int IGNORED_PORT_NUMBER = -1; @@ -48,7 +47,6 @@ public class NettyConnection implements Connection, TimeToFirstByteListener { private final Channel channel; private final HttpRequestOperationFactory requestOperationFactory; - private volatile long timeToFirstByteMs; private final Announcer listeners = Announcer.to(Listener.class); @@ -68,7 +66,6 @@ public NettyConnection(Origin origin, Channel channel, HttpRequestOperationFacto this.origin = requireNonNull(origin); this.channel = requireNonNull(channel); this.requestOperationFactory = requestOperationFactory; - this.channel.pipeline().addLast(new TimeToFirstByteHandler(this)); this.channel.closeFuture().addListener(future -> listeners.announce().connectionClosed(NettyConnection.this)); addChannelHandlers(channel, httpConfig, sslContext, sendSni, sniHost.orElse(origin.host())); @@ -115,11 +112,6 @@ public Origin getOrigin() { return this.origin; } - @Override - public long getTimeToFirstByteMillis() { - return this.timeToFirstByteMs; - } - @Override public void addConnectionListener(Listener listener) { this.listeners.addListener(listener); @@ -133,11 +125,6 @@ public void close() { } } - @Override - public void notifyTimeToFirstByte(long timeToFirstByte, TimeUnit timeUnit) { - this.timeToFirstByteMs = timeUnit.toMillis(timeToFirstByte); - } - @Override public String toString() { return toStringHelper(this) diff --git a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/RequestsToOriginMetricsCollector.java b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/RequestsToOriginMetricsCollector.java index c087670876..be9304e1cd 100644 --- a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/RequestsToOriginMetricsCollector.java +++ b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/RequestsToOriginMetricsCollector.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; import org.slf4j.Logger; @@ -38,6 +39,9 @@ class RequestsToOriginMetricsCollector extends ChannelDuplexHandler { private final OriginStats originStats; private volatile AggregateTimer.Stopper requestLatencyTiming; + private volatile AggregateTimer.Stopper timeToFirstByteTiming; + private volatile boolean firstContentChunkReceived; + /** * Constructs a new instance. @@ -62,6 +66,11 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception updateHttpResponseCounters(code); } + if (msg instanceof HttpContent && !firstContentChunkReceived) { + stopAndRecordTimeToFirstByte(); + firstContentChunkReceived = true; + } + if (msg instanceof LastHttpContent) { stopAndRecordLatency(); } @@ -73,6 +82,8 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (msg instanceof io.netty.handler.codec.http.HttpRequest) { requestLatencyTiming = originStats.requestLatencyTimer().time(); + timeToFirstByteTiming = originStats.timeToFirstByteTimer().time(); + firstContentChunkReceived = false; } super.write(ctx, msg, promise); } @@ -93,6 +104,7 @@ private void updateHttpResponseCounters(int statusCode) { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { stopAndRecordLatency(); + stopAndRecordTimeToFirstByte(); super.exceptionCaught(ctx, cause); } @@ -106,4 +118,12 @@ private void stopAndRecordLatency() { LOG.warn("Attempted to stop timer and record latency when no timing had begun"); } } + + private void stopAndRecordTimeToFirstByte() { + if (timeToFirstByteTiming != null) { + timeToFirstByteTiming.stopAndRecord(); + } else { + LOG.warn("Attempted to stop timer and record time-to-first-byte when no timing had begun"); + } + } } diff --git a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/TimeToFirstByteHandler.java b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/TimeToFirstByteHandler.java deleted file mode 100644 index 0ff4ab6593..0000000000 --- a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/TimeToFirstByteHandler.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - Copyright (C) 2013-2018 Expedia Inc. - - 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.hotels.styx.client.netty.connectionpool; - -import com.hotels.styx.api.Clock; -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.http.HttpContent; -import io.netty.handler.codec.http.HttpRequest; - -import static com.hotels.styx.api.Clocks.systemClock; -import static java.util.Objects.requireNonNull; -import static java.util.concurrent.TimeUnit.MILLISECONDS; - -/** - * A netty channel handler that provides the {@link TimeToFirstByteListener} with a measurement of time-to-first-byte in milliseconds. - */ -class TimeToFirstByteHandler extends ChannelDuplexHandler { - private final TimeToFirstByteListener timeToFirstByteListener; - - private boolean firstChunkReceived; - private long startTimeMs; - private final Clock clock; - - TimeToFirstByteHandler(TimeToFirstByteListener timeToFirstByteListener) { - this(timeToFirstByteListener, systemClock()); - } - - TimeToFirstByteHandler(TimeToFirstByteListener timeToFirstByteListener, Clock clock) { - this.timeToFirstByteListener = requireNonNull(timeToFirstByteListener); - this.clock = requireNonNull(clock); - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof HttpContent && !this.firstChunkReceived) { - timeToFirstByteListener.notifyTimeToFirstByte(clock.tickMillis() - startTimeMs, MILLISECONDS); - firstChunkReceived = true; - } - super.channelRead(ctx, msg); - } - - @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - if (msg instanceof HttpRequest) { - startTimeMs = clock.tickMillis(); - firstChunkReceived = false; - } - super.write(ctx, msg, promise); - } -} diff --git a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/TimeToFirstByteListener.java b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/TimeToFirstByteListener.java deleted file mode 100644 index 8f1f2dd3fe..0000000000 --- a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/TimeToFirstByteListener.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - Copyright (C) 2013-2018 Expedia Inc. - - 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.hotels.styx.client.netty.connectionpool; - -import java.util.concurrent.TimeUnit; - -/** - * A receiver of connection statistics. - */ -public interface TimeToFirstByteListener { - /** - * Sets time to first byte in milliseconds. - * - * @param timeToFirstByte time to first byte - */ - void notifyTimeToFirstByte(long timeToFirstByte, TimeUnit timeUnit); -} diff --git a/components/client/src/test/unit/java/com/hotels/styx/client/connectionpool/StatsReportingConnectionPoolTest.java b/components/client/src/test/unit/java/com/hotels/styx/client/connectionpool/StatsReportingConnectionPoolTest.java index 3e15dd6460..64e901d5ce 100644 --- a/components/client/src/test/unit/java/com/hotels/styx/client/connectionpool/StatsReportingConnectionPoolTest.java +++ b/components/client/src/test/unit/java/com/hotels/styx/client/connectionpool/StatsReportingConnectionPoolTest.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/RequestsToOriginMetricsCollectorTest.java b/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/RequestsToOriginMetricsCollectorTest.java index 422fbad169..b0300fbd64 100644 --- a/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/RequestsToOriginMetricsCollectorTest.java +++ b/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/RequestsToOriginMetricsCollectorTest.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -303,6 +303,41 @@ public void latencyHistogramUpdatedOnlyByLastHttpContent() { assertThat(timer.getCount(), is(1L)); } + @Test + public void timeToFirstByteHistogramUpdatedWhenFirstContentChunkReceived() { + + Timer timer = this.metricRegistry.timer(name(ORIGIN_METRIC_PREFIX, "requests.time-to-first-byte")); + assertThat(timer.getCount(), is(0L)); + + EmbeddedChannel channel = buildEmbeddedChannel(); + + // + // Send out a HttpRequest in outbound direction, towards origins: + // + HttpRequest request = httpRequest(GET, "http://www.hotels.com/foo/bar/request"); + channel.writeOutbound(request); + assertThat(grabSentBytes(channel).isPresent(), is(true)); + assertThat(timer.getCount(), is(0L)); + + ByteBuf response = httpResponseAsBuf(OK, STOCK_BODY).retain(); + int len = response.writerIndex() - response.readerIndex(); + + // + // Send the response in two chunks. The timer is updated immediately when + // the first chunk is received: + // + channel.writeInbound(response.slice(0, 100)); + assertThat(timer.getCount(), is(1L)); + + // + // Send the next chunk. Demonstrate that timer remains unchanged. This is to ensure + // it doesn't get recorded twice: + // + channel.writeInbound(response.slice(100, len - 100)); + assertThat(timer.getCount(), is(1L)); + } + + @Test public void response100ContinueUpdatesInformationalMeterOnly() { EmbeddedChannel channel = buildEmbeddedChannel(); diff --git a/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/StubConnectionPool.java b/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/StubConnectionPool.java index 973810d947..9624347e2c 100644 --- a/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/StubConnectionPool.java +++ b/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/StubConnectionPool.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32,7 +32,6 @@ public class StubConnectionPool implements ConnectionPool, Comparable`.registered-channel-count** @@ -191,6 +190,14 @@ The origin side metrics scopes are illustrated in a diagram below: * Timer started when request is sent, and stopped when the last content byte is received. +**origins.``.requests.time-to-first-byte** +**origins.``.``.requests.time-to-first-byte** + +* A latency distribution of requests to origin. +* Measured as time to first content byte. +* Timer started when request is sent, and stopped when the first content + byte is received. + **origins.``.``.status** * Current [origin state](configure-health-checks.md), as follows: diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/FileBasedOriginsFileChangeMonitorSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/FileBasedOriginsFileChangeMonitorSpec.kt index 7403beff18..815be4d6a4 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/FileBasedOriginsFileChangeMonitorSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/FileBasedOriginsFileChangeMonitorSpec.kt @@ -53,8 +53,10 @@ class FileBasedOriginsFileChangeMonitorSpec: StringSpec() { .header(HOST, styxServer().proxyHttpHostHeader()) .build() - client.send(reqToApp01).wait().status() shouldBe OK - client.send(reqToApp02).wait().status() shouldBe BAD_GATEWAY + eventually(3.seconds, AssertionError::class.java) { + client.send(reqToApp01).wait().status() shouldBe OK + client.send(reqToApp02).wait().status() shouldBe BAD_GATEWAY + } writeConfig(styxOriginsFile, configTemplate.format("appv2", "/app02/", mockServer.port())) diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/MetricsSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/MetricsSpec.kt index 27b407461c..19ba3bef80 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/MetricsSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/MetricsSpec.kt @@ -29,6 +29,9 @@ import com.hotels.styx.support.testClient import com.hotels.styx.support.wait import io.kotlintest.Spec import io.kotlintest.eventually +import io.kotlintest.matchers.doubles.shouldBeGreaterThan +import io.kotlintest.matchers.numerics.shouldBeGreaterThan +import io.kotlintest.matchers.numerics.shouldBeInRange import io.kotlintest.seconds import io.kotlintest.shouldBe import io.kotlintest.specs.FunSpec @@ -99,6 +102,15 @@ class MetricsSpec : FunSpec() { name shouldBe "origins.appA.appA-01.connectionspool.available-connections" } } + + test("time-to-first-byte metrics are reported") { + styxServer().metrics().let { + (it["origins.appA.appA-01.requests.time-to-first-byte"]!!["count"] as Int) shouldBeGreaterThan 0 + (it["origins.appA.appA-01.requests.time-to-first-byte"]!!["mean"] as Double) shouldBeGreaterThan 0.0 + (it["origins.requests.time-to-first-byte"]!!["count"] as Int) shouldBeGreaterThan 0 + (it["origins.requests.time-to-first-byte"]!!["mean"] as Double) shouldBeGreaterThan 0.0 + } + } } }