diff --git a/components/proxy/src/main/java/com/hotels/styx/routing/config/RoutingObjectFactory.java b/components/proxy/src/main/java/com/hotels/styx/routing/config/RoutingObjectFactory.java index 55fed3bda2..0eb6408af0 100644 --- a/components/proxy/src/main/java/com/hotels/styx/routing/config/RoutingObjectFactory.java +++ b/components/proxy/src/main/java/com/hotels/styx/routing/config/RoutingObjectFactory.java @@ -26,6 +26,7 @@ import com.hotels.styx.routing.RoutingObjectRecord; import com.hotels.styx.routing.db.StyxObjectStore; import com.hotels.styx.routing.handlers.ConditionRouter; +import com.hotels.styx.routing.handlers.HostProxy; import com.hotels.styx.routing.handlers.HttpInterceptorPipeline; import com.hotels.styx.routing.handlers.PathPrefixRouter; import com.hotels.styx.routing.handlers.ProxyToBackend; @@ -58,6 +59,7 @@ public class RoutingObjectFactory { private static final String INTERCEPTOR_PIPELINE = "InterceptorPipeline"; private static final String PROXY_TO_BACKEND = "ProxyToBackend"; private static final String PATH_PREFIX_ROUTER = "PathPrefixRouter"; + private static final String HOST_PROXY = "HostProxy"; static { @@ -67,6 +69,7 @@ public class RoutingObjectFactory { .put(INTERCEPTOR_PIPELINE, new HttpInterceptorPipeline.Factory()) .put(PROXY_TO_BACKEND, new ProxyToBackend.Factory()) .put(PATH_PREFIX_ROUTER, new PathPrefixRouter.Factory()) + .put(HOST_PROXY, new HostProxy.Factory()) .build(); BUILTIN_HANDLER_SCHEMAS = ImmutableMap.builder() @@ -75,6 +78,7 @@ public class RoutingObjectFactory { .put(INTERCEPTOR_PIPELINE, HttpInterceptorPipeline.SCHEMA) .put(PROXY_TO_BACKEND, ProxyToBackend.SCHEMA) .put(PATH_PREFIX_ROUTER, PathPrefixRouter.SCHEMA) + .put(HOST_PROXY, HostProxy.SCHEMA) .build(); } diff --git a/components/proxy/src/main/java/com/hotels/styx/routing/handlers/HostProxy.java b/components/proxy/src/main/java/com/hotels/styx/routing/handlers/HostProxy.java new file mode 100644 index 0000000000..57a3449183 --- /dev/null +++ b/components/proxy/src/main/java/com/hotels/styx/routing/handlers/HostProxy.java @@ -0,0 +1,238 @@ +/* + Copyright (C) 2013-2019 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.routing.handlers; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HostAndPort; +import com.hotels.styx.api.Eventual; +import com.hotels.styx.api.HttpInterceptor; +import com.hotels.styx.api.LiveHttpRequest; +import com.hotels.styx.api.LiveHttpResponse; +import com.hotels.styx.api.extension.Origin; +import com.hotels.styx.api.extension.service.ConnectionPoolSettings; +import com.hotels.styx.api.extension.service.TlsSettings; +import com.hotels.styx.client.Connection; +import com.hotels.styx.client.OriginStatsFactory; +import com.hotels.styx.client.StyxHostHttpClient; +import com.hotels.styx.client.applications.metrics.OriginMetrics; +import com.hotels.styx.client.connectionpool.ConnectionPool; +import com.hotels.styx.client.connectionpool.ExpiringConnectionFactory; +import com.hotels.styx.client.connectionpool.SimpleConnectionPoolFactory; +import com.hotels.styx.client.netty.connectionpool.NettyConnectionFactory; +import com.hotels.styx.config.schema.Schema; +import com.hotels.styx.infrastructure.configuration.yaml.JsonNodeConfig; +import com.hotels.styx.routing.RoutingObject; +import com.hotels.styx.routing.config.HttpHandlerFactory; +import com.hotels.styx.routing.config.RoutingObjectDefinition; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import static com.hotels.styx.api.extension.Origin.newOriginBuilder; +import static com.hotels.styx.api.extension.service.ConnectionPoolSettings.defaultConnectionPoolSettings; +import static com.hotels.styx.client.HttpRequestOperationFactory.Builder.httpRequestOperationFactoryBuilder; +import static com.hotels.styx.config.schema.SchemaDsl.atLeastOne; +import static com.hotels.styx.config.schema.SchemaDsl.bool; +import static com.hotels.styx.config.schema.SchemaDsl.field; +import static com.hotels.styx.config.schema.SchemaDsl.integer; +import static com.hotels.styx.config.schema.SchemaDsl.list; +import static com.hotels.styx.config.schema.SchemaDsl.object; +import static com.hotels.styx.config.schema.SchemaDsl.optional; +import static com.hotels.styx.config.schema.SchemaDsl.string; +import static com.hotels.styx.routing.config.RoutingSupport.missingAttributeError; +import static java.lang.String.format; +import static java.lang.String.join; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.CompletableFuture.completedFuture; + +/** + * A routing object that proxies all incoming traffic to a remote host. + */ +public class HostProxy implements RoutingObject { + public static final Schema.FieldType SCHEMA = object( + field("host", string()), + optional("tlsSettings", object( + optional("trustAllCerts", bool()), + optional("sslProvider", string()), + optional("trustStorePath", string()), + optional("trustStorePassword", string()), + optional("protocols", list(string())), + optional("cipherSuites", list(string())), + optional("additionalCerts", list(object( + field("alias", string()), + field("certificatePath", string()) + ))), + atLeastOne("trustAllCerts", + "trustStorePath", + "trustStorePassword", + "protocols", + "cipherSuites", + "additionalCerts") + )), + optional("connectionPool", object( + optional("maxConnections", integer()), + optional("maxPendingConnections", integer()), + optional("connectTimeoutMillis", integer()), + optional("socketTimeoutMillis", integer()), + optional("pendingConnectionTimeoutMillis", integer()), + optional("connectionExpirationSeconds", integer()), + atLeastOne("maxConnections", + "maxPendingConnections", + "connectTimeoutMillis", + "socketTimeoutMillis", + "pendingConnectionTimeoutMillis", + "connectionExpirationSeconds") + )), + optional("responseTimeoutMillis", integer()), + optional("metricPrefix", string()) + ); + + private final String errorMessage; + private final StyxHostHttpClient client; + private volatile boolean active = true; + + @VisibleForTesting + final HostAndPort hostAndPort; + + public HostProxy(HostAndPort hostAndPort, StyxHostHttpClient client) { + this.hostAndPort = requireNonNull(hostAndPort); + this.errorMessage = format("HostProxy %s:%d is stopped but received traffic.", + hostAndPort.getHostText(), + hostAndPort.getPort()); + this.client = requireNonNull(client); + } + + @Override + public Eventual handle(LiveHttpRequest request, HttpInterceptor.Context context) { + if (active) { + return new Eventual<>(client.sendRequest(request)); + } else { + return Eventual.error(new IllegalStateException(errorMessage)); + } + } + + @Override + public CompletableFuture stop() { + active = false; + client.close(); + return completedFuture(null); + } + + /** + * A factory for creating HostProxy routingObject objects. + */ + public static class Factory implements HttpHandlerFactory { + private static final int DEFAULT_REQUEST_TIMEOUT = 60000; + private static final int DEFAULT_TLS_PORT = 443; + private static final int DEFAULT_HTTP_PORT = 80; + + @Override + public RoutingObject build(List parents, Context context, RoutingObjectDefinition configBlock) { + JsonNodeConfig config = new JsonNodeConfig(configBlock.config()); + + ConnectionPoolSettings poolSettings = config.get("connectionPool", ConnectionPoolSettings.class) + .orElse(defaultConnectionPoolSettings()); + + TlsSettings tlsSettings = config.get("tlsSettings", TlsSettings.class) + .orElse(null); + + int responseTimeoutMillis = config.get("responseTimeoutMillis", Integer.class) + .orElse(DEFAULT_REQUEST_TIMEOUT); + + String metricPrefix = config.get("metricPrefix", String.class) + .orElse("routing.objects.hostProxy"); + + HostAndPort hostAndPort = config.get("host") + .map(HostAndPort::fromString) + .map(it -> addDefaultPort(it, tlsSettings)) + .orElseThrow(() -> missingAttributeError(configBlock, join(".", parents), "host")); + + return createHostProxyHandler(context, hostAndPort, poolSettings, tlsSettings, responseTimeoutMillis, metricPrefix); + } + + private static HostAndPort addDefaultPort(HostAndPort hostAndPort, TlsSettings tlsSettings) { + if (hostAndPort.hasPort()) { + return hostAndPort; + } + + int defaultPort = Optional.ofNullable(tlsSettings) + .map(it -> DEFAULT_TLS_PORT) + .orElse(DEFAULT_HTTP_PORT); + + return HostAndPort.fromParts(hostAndPort.getHostText(), defaultPort); + } + + @NotNull + private static HostProxy createHostProxyHandler( + Context context, + HostAndPort hostAndPort, + ConnectionPoolSettings poolSettings, + TlsSettings tlsSettings, + int responseTimeoutMillis, + String metricPrefix) { + + Origin origin = newOriginBuilder(hostAndPort.getHostText(), hostAndPort.getPort()) + .applicationId(metricPrefix) + .id(format("%s:%s", hostAndPort.getHostText(), hostAndPort.getPort())) + .build(); + + OriginMetrics originMetrics = OriginMetrics.create( + origin, + context.environment().metricRegistry()); + + ConnectionPool.Factory connectionPoolFactory = new SimpleConnectionPoolFactory.Builder() + .connectionFactory( + connectionFactory( + tlsSettings, + responseTimeoutMillis, + theOrigin -> originMetrics, + poolSettings.connectionExpirationSeconds())) + .connectionPoolSettings(poolSettings) + .metricRegistry(context.environment().metricRegistry()) + .build(); + + return new HostProxy(hostAndPort, StyxHostHttpClient.create(connectionPoolFactory.create(origin))); + } + + private static Connection.Factory connectionFactory( + TlsSettings tlsSettings, + int responseTimeoutMillis, + OriginStatsFactory originStatsFactory, + long connectionExpiration) { + + NettyConnectionFactory factory = new NettyConnectionFactory.Builder() + .httpRequestOperationFactory( + httpRequestOperationFactoryBuilder() + .flowControlEnabled(true) + .originStatsFactory(originStatsFactory) + .responseTimeoutMillis(responseTimeoutMillis) + .build() + ) + .tlsSettings(Optional.ofNullable(tlsSettings).orElse(null)) + .build(); + + if (connectionExpiration > 0) { + return new ExpiringConnectionFactory(connectionExpiration, factory); + } else { + return factory; + } + } + + } + +} diff --git a/components/proxy/src/test/kotlin/com/hotels/styx/routing/Support.kt b/components/proxy/src/test/kotlin/com/hotels/styx/routing/Support.kt index 3e44845cc6..7c1d7a0eaf 100644 --- a/components/proxy/src/test/kotlin/com/hotels/styx/routing/Support.kt +++ b/components/proxy/src/test/kotlin/com/hotels/styx/routing/Support.kt @@ -41,7 +41,7 @@ import java.util.concurrent.CompletableFuture fun routingObjectDef(text: String) = YamlConfig(text).`as`((RoutingObjectDefinition::class.java)) data class RoutingContext( - val environment: Environment = mockk(), + val environment: Environment = Environment.Builder().build(), val routeDb: StyxObjectStore = StyxObjectStore(), val factory: RoutingObjectFactory = routingObjectFactory(), val plugins: Iterable = listOf(), diff --git a/components/proxy/src/test/kotlin/com/hotels/styx/routing/handlers/HostProxyTest.kt b/components/proxy/src/test/kotlin/com/hotels/styx/routing/handlers/HostProxyTest.kt new file mode 100644 index 0000000000..e2c1a50ca0 --- /dev/null +++ b/components/proxy/src/test/kotlin/com/hotels/styx/routing/handlers/HostProxyTest.kt @@ -0,0 +1,129 @@ +/* + Copyright (C) 2013-2019 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.routing.handlers + +import com.google.common.net.HostAndPort +import com.hotels.styx.api.Eventual +import com.hotels.styx.api.HttpRequest +import com.hotels.styx.api.HttpResponse +import com.hotels.styx.api.HttpResponseStatus.OK +import com.hotels.styx.api.LiveHttpRequest +import com.hotels.styx.client.StyxHostHttpClient +import com.hotels.styx.routing.RoutingContext +import com.hotels.styx.routing.handle +import com.hotels.styx.routing.routingObjectDef +import io.kotlintest.TestCase +import io.kotlintest.shouldBe +import io.kotlintest.shouldThrow +import io.kotlintest.specs.FeatureSpec +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import reactor.core.publisher.toMono + +class HostProxyTest : FeatureSpec() { + val request = HttpRequest.get("/").build() + var client: StyxHostHttpClient? = null + + init { + feature("Routing and proxying") { + scenario("Proxies traffic") { + HostProxy(HostAndPort.fromString("localhost:80"), client).handle(request.stream(), mockk()) + + verify { + client?.sendRequest(ofType(LiveHttpRequest::class)) + } + } + + scenario("Requests arriving at stopped HostProxy object") { + val exception = HostProxy(HostAndPort.fromString("localhost:80"), client).let { + it.stop() + + shouldThrow { + it.handle(request) + .toMono() + .block() + } + } + + exception.message shouldBe ("HostProxy localhost:80 is stopped but received traffic.") + + verify(exactly = 0) { + client?.sendRequest(any()) + } + } + } + + feature("HostProxy.Factory") { + + val context = RoutingContext() + + scenario("Uses configured host and port number") { + val factory = HostProxy.Factory() + + val hostProxy = factory.build(listOf(), context.get(), routingObjectDef(""" + type: HostProxy + config: + host: ahost.server.com:1234 + """.trimIndent())) as HostProxy + + hostProxy.hostAndPort.run { + hostText shouldBe "ahost.server.com" + port shouldBe 1234 + } + } + + scenario("Port defaults to 80") { + val factory = HostProxy.Factory() + + val hostProxy = factory.build(listOf(), context.get(), routingObjectDef(""" + type: HostProxy + config: + host: localhost + """.trimIndent())) as HostProxy + + hostProxy.hostAndPort.run { + hostText shouldBe "localhost" + port shouldBe 80 + } + } + + scenario("Port defaults to 443 when TLS settings are present") { + val factory = HostProxy.Factory() + + val hostProxy = factory.build(listOf(), context.get(), routingObjectDef(""" + type: HostProxy + config: + host: localhost + tlsSettings: + trustAllCerts: true + """.trimIndent())) as HostProxy + + hostProxy.hostAndPort.run { + hostText shouldBe "localhost" + port shouldBe 443 + } + } + } + } + + override fun beforeTest(testCase: TestCase) { + client = mockk(relaxed = true) { + every { sendRequest(any()) } returns Eventual.of(HttpResponse.response(OK).build().stream()) + } + } + +} \ No newline at end of file diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/resiliency/OriginResourcesSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/resiliency/OriginResourcesSpec.kt index 0fb0f61c38..4f5b9282cf 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/resiliency/OriginResourcesSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/resiliency/OriginResourcesSpec.kt @@ -24,6 +24,7 @@ import com.hotels.styx.server.HttpConnectorConfig import com.hotels.styx.servers.MockOriginServer import com.hotels.styx.support.StyxServerProvider import com.hotels.styx.support.proxyHttpHostHeader +import com.hotels.styx.support.threadCount import com.hotels.styx.support.wait import io.kotlintest.Spec import io.kotlintest.eventually @@ -167,11 +168,6 @@ class OriginResourcesSpec : StringSpec() { - { id: "$prefix", host: "localhost:${mockServer.port()}" } """.trimIndent() - fun threadCount(namePattern: String) = Thread.getAllStackTraces().keys - .map { it.name } - .filter { it.contains(namePattern) } - .count() - fun configurationApplied(prefix: String) = client.send(get(prefix).header(HOST, styxServer().proxyHttpHostHeader()).build()) .wait(debug = false) .status() == OK diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/HostProxySpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/HostProxySpec.kt new file mode 100644 index 0000000000..bca9318858 --- /dev/null +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/HostProxySpec.kt @@ -0,0 +1,417 @@ +/* + Copyright (C) 2013-2019 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.routing + +import com.github.tomakehurst.wiremock.client.WireMock +import com.github.tomakehurst.wiremock.client.WireMock.aResponse +import com.github.tomakehurst.wiremock.client.WireMock.urlMatching +import com.hotels.styx.api.HttpHeaderNames.HOST +import com.hotels.styx.api.HttpRequest.get +import com.hotels.styx.api.HttpResponseStatus +import com.hotels.styx.api.HttpResponseStatus.CREATED +import com.hotels.styx.api.HttpResponseStatus.GATEWAY_TIMEOUT +import com.hotels.styx.api.HttpResponseStatus.OK +import com.hotels.styx.client.StyxHttpClient +import com.hotels.styx.server.HttpConnectorConfig +import com.hotels.styx.servers.MockOriginServer +import com.hotels.styx.support.ResourcePaths +import com.hotels.styx.support.StyxServerProvider +import com.hotels.styx.support.metrics +import com.hotels.styx.support.newRoutingObject +import com.hotels.styx.support.proxyHttpHostHeader +import com.hotels.styx.support.proxyHttpsHostHeader +import com.hotels.styx.support.removeRoutingObject +import com.hotels.styx.support.threadCount +import com.hotels.styx.support.wait +import io.kotlintest.IsolationMode +import io.kotlintest.Spec +import io.kotlintest.eventually +import io.kotlintest.matchers.beGreaterThan +import io.kotlintest.matchers.beLessThan +import io.kotlintest.matchers.numerics.shouldBeInRange +import io.kotlintest.matchers.types.shouldBeNull +import io.kotlintest.seconds +import io.kotlintest.shouldBe +import io.kotlintest.specs.FeatureSpec +import java.nio.charset.StandardCharsets.UTF_8 +import kotlin.system.measureTimeMillis + +class HostProxySpec : FeatureSpec() { + + // Enforce one instance for the test spec. + // Run the tests sequentially: + override fun isolationMode(): IsolationMode = IsolationMode.SingleInstance + + val originsOk = ResourcePaths.fixturesHome(ConditionRoutingSpec::class.java, "/conf/origins/origins-correct.yml") + + override fun beforeSpec(spec: Spec) { + testServer.restart() + styxServer.restart() + } + + override fun afterSpec(spec: Spec) { + styxServer.stop() + mockServer.stop() + testServer.stop() + } + + init { + + feature("Executor thread pool") { + scenario("Runs on StyxHttpClient global thread pool") { + testServer.restart() + styxServer.restart() + + for (i in 1..4) { + styxServer().newRoutingObject("hostProxy", """ + type: HostProxy + config: + host: localhost:${mockServer.port()} + """.trimIndent()) shouldBe CREATED + + client.send(get("/").header(HOST, styxServer().proxyHttpHostHeader()).build()) + .wait() + .status() shouldBe OK + + styxServer().removeRoutingObject("hostProxy") + } + + threadCount("Styx-Client-Global") shouldBe 2 * Runtime.getRuntime().availableProcessors(); + } + } + + feature("Proxying requests") { + scenario("Response Timeout") { + styxServer().newRoutingObject("hostProxy", """ + type: HostProxy + config: + host: localhost:${mockServer.port()} + responseTimeoutMillis: 600 + """.trimIndent()) shouldBe CREATED + + measureTimeMillis { + client.send(get("/slow/n") + .header(HOST, styxServer().proxyHttpHostHeader()) + .build()) + .wait() + .status() shouldBe GATEWAY_TIMEOUT + }.let { delay -> + delay shouldBe (beGreaterThan(600) and beLessThan(1000)) + } + } + + scenario("Applies TLS settings") { + styxServer().newRoutingObject("hostProxy", """ + type: HostProxy + config: + host: ${testServer.get().proxyHttpsHostHeader()} + tlsSettings: + trustAllCerts: true + sslProvider: JDK + """.trimIndent()) + + client.send(get("/") + .header(HOST, styxServer().proxyHttpHostHeader()) + .build()) + .wait() + .let { + it?.status() shouldBe OK + it?.bodyAs(UTF_8) shouldBe "Hello - HTTPS" + } + } + } + + + feature("Connection pooling") { + scenario("Pools connections") { + testServer.restart() + styxServer.restart() + + styxServer().newRoutingObject("hostProxy", """ + type: HostProxy + config: + host: localhost:${testServer().proxyHttpAddress().port} + connectionPool: + maxConnectionsPerHost: 2 + maxPendingConnectionsPerHost: 10 + """.trimIndent()) shouldBe CREATED + + val requestFutures = (1..10).map { client.send(get("/").header(HOST, styxServer().proxyHttpHostHeader()).build()) } + + requestFutures + .forEach { + val clientResponse = it.wait() + clientResponse?.status() shouldBe HttpResponseStatus.OK + clientResponse?.bodyAs(UTF_8) shouldBe "Hello - HTTP" + } + + testServer().metrics().let { + (it["connections.total-connections"]?.get("count") as Int) shouldBeInRange 1..2 + } + + styxServer().metrics().let { + (it["routing.objects.hostProxy.localhost:${testServer().proxyHttpAddress().port}.connectionspool.connection-attempts"]?.get("value") as Int) shouldBeInRange 1..2 + } + + } + + scenario("Applies connection expiration settings") { + val connectinExpiryInSeconds = 1 + testServer.restart() + styxServer.restart() + + styxServer().newRoutingObject("hostProxy", """ + type: HostProxy + config: + host: ${testServer().proxyHttpHostHeader()} + connectionPool: + maxConnectionsPerHost: 2 + maxPendingConnectionsPerHost: 10 + connectionExpirationSeconds: $connectinExpiryInSeconds + """.trimIndent()) shouldBe CREATED + + client.send(get("/") + .header(HOST, styxServer().proxyHttpHostHeader()) + .build()) + .wait() + .status() shouldBe OK + + eventually(1.seconds, AssertionError::class.java) { + styxServer().metrics().let { + it["routing.objects.hostProxy.localhost:${testServer().proxyHttpAddress().port}.connectionspool.available-connections"]?.get("value") shouldBe 1 + it["routing.objects.hostProxy.localhost:${testServer().proxyHttpAddress().port}.connectionspool.connections-closed"]?.get("value") shouldBe 0 + } + } + + // Wait for connection to expiry + Thread.sleep(connectinExpiryInSeconds*1000L) + + client.send(get("/") + .header(HOST, styxServer().proxyHttpHostHeader()) + .build()) + .wait() + .status() shouldBe OK + + eventually(1.seconds, AssertionError::class.java) { + styxServer().metrics().let { + it["routing.objects.hostProxy.localhost:${testServer().proxyHttpAddress().port}.connectionspool.available-connections"]?.get("value") shouldBe 1 + it["routing.objects.hostProxy.localhost:${testServer().proxyHttpAddress().port}.connectionspool.connections-terminated"]?.get("value") shouldBe 1 + } + } + + } + } + + + feature("Metrics collecting") { + + scenario("Restart servers and configure hostProxy object") { + testServer.restart() + styxServer.restart() + + styxServer().newRoutingObject("hostProxy", """ + type: HostProxy + config: + host: localhost:${mockServer.port()} + """.trimIndent()) shouldBe CREATED + } + + // Continues from previous test + scenario("Send request") { + client.send(get("/") + .header(HOST, styxServer().proxyHttpHostHeader()) + .build()) + .wait() + .let { + it?.status() shouldBe HttpResponseStatus.OK + it?.bodyAs(UTF_8) shouldBe "mock-server-01" + } + } + + // Continues from previous test + scenario("Provides connection pool metrics") { + styxServer().metrics().let { + it["routing.objects.hostProxy.localhost:${mockServer.port()}.connectionspool.connection-attempts"]?.get("value") shouldBe 1 + } + } + + // Continues from previous test + scenario("Provides origin and application metrics") { + styxServer().metrics().let { + it["routing.objects.hostProxy.localhost:${mockServer.port()}.requests.response.status.200"]?.get("count") shouldBe 1 + it["routing.objects.hostProxy.localhost:${mockServer.port()}.connectionspool.connection-attempts"]?.get("value") shouldBe 1 + } + } + + // Continues from previous test + scenario("Unregisters connection pool metrics") { + styxServer().removeRoutingObject("hostProxy") + + eventually(2.seconds, AssertionError::class.java) { + styxServer().metrics().let { + it["routing.objects.hostProxy.localhost:${mockServer.port()}.connectionspool.connection-attempts"].shouldBeNull() + } + } + } + + // Continues from previous test + scenario("!Unregisters origin/application metrics") { + // TODO: Not supported yet. An existing issue within styx. + + eventually(2.seconds, AssertionError::class.java) { + styxServer().metrics().let { + it["routing.objects.hostProxy.requests.response.status.200"].shouldBeNull() + it["routing.objects.hostProxy.localhost:${mockServer.port()}.requests.response.status.200"].shouldBeNull() + } + } + } + } + + + feature("Metrics collecting with metric prefix") { + + scenario("Restart servers and configure hostProxy object with metric prefix") { + testServer.restart() + styxServer.restart() + + styxServer().newRoutingObject("hostProxy", """ + type: HostProxy + config: + host: localhost:${mockServer.port()} + metricPrefix: origins.myApp + """.trimIndent()) shouldBe CREATED + } + + // Continues from previous test + scenario("Send request") { + client.send(get("/") + .header(HOST, styxServer().proxyHttpHostHeader()) + .build()) + .wait() + .let { + it?.status() shouldBe HttpResponseStatus.OK + it?.bodyAs(UTF_8) shouldBe "mock-server-01" + } + } + + // Continues from previous test + scenario("Provides connection pool metrics with metric prefix") { + styxServer().metrics().let { + it["origins.myApp.localhost:${mockServer.port()}.connectionspool.connection-attempts"]?.get("value") shouldBe 1 + } + } + + // Continues from previous test + scenario("Provides origin/application metrics with metric prefix") { + styxServer().metrics().let { + it["origins.myApp.localhost:${mockServer.port()}.requests.response.status.200"]?.get("count") shouldBe 1 + it["origins.myApp.requests.response.status.200"]?.get("count") shouldBe 1 + } + } + + // Continues from previous test + scenario("Unregisters prefixed connection pool metrics") { + styxServer().removeRoutingObject("hostProxy") + + eventually(2.seconds, AssertionError::class.java) { + styxServer().metrics().let { + it["origins.myApp.localhost:${mockServer.port()}.connectionspool.connection-attempts"].shouldBeNull() + } + } + } + + // Continues from previous test + scenario("!Unregisters prefixed origin/application metrics") { + // TODO: Not supported yet. An existing issue within styx. + eventually(2.seconds, AssertionError::class.java) { + styxServer().metrics().let { + it["origins.myApp.localhost:${mockServer.port()}.requests.response.status.200"].shouldBeNull() + it["origins.myApp.requests.response.status.200"].shouldBeNull() + } + } + } + } + } + + private val styxServer = StyxServerProvider(""" + proxy: + connectors: + http: + port: 0 + clientWorkerThreadsCount: 3 + + admin: + connectors: + http: + port: 0 + + services: + factories: + backendServiceRegistry: + class: "com.hotels.styx.proxy.backends.file.FileBackedBackendServicesRegistry${'$'}Factory" + config: {originsFile: "$originsOk"} + + httpPipeline: hostProxy + """.trimIndent()) + + private val testServer = StyxServerProvider(""" + proxy: + connectors: + http: + port: 0 + https: + port: 0 + + admin: + connectors: + http: + port: 0 + + services: + factories: + backendServiceRegistry: + class: "com.hotels.styx.proxy.backends.file.FileBackedBackendServicesRegistry${'$'}Factory" + config: {originsFile: "$originsOk"} + + httpPipeline: + type: ConditionRouter + config: + routes: + - condition: protocol() == "https" + destination: + type: StaticResponseHandler + config: + status: 200 + content: "Hello - HTTPS" + fallback: + type: StaticResponseHandler + config: + status: 200 + content: "Hello - HTTP" + """.trimIndent()) + + val client: StyxHttpClient = StyxHttpClient.Builder().build() + + val mockServer = MockOriginServer.create("", "", 0, HttpConnectorConfig(0)) + .start() + .stub(WireMock.get(urlMatching("/.*")), aResponse() + .withStatus(200) + .withBody("mock-server-01")) + .stub(WireMock.get(urlMatching("/slow/.*")), aResponse() + .withStatus(200) + .withFixedDelay(1500) + .withBody("mock-server-01 slow")) +} diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/support/StyxServerProvider.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/support/StyxServerProvider.kt index 316e177d73..e61cefafdf 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/support/StyxServerProvider.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/support/StyxServerProvider.kt @@ -136,4 +136,9 @@ fun StyxServer.removeRoutingObject(name: String): HttpResponseStatus { } return response?.status() ?: HttpResponseStatus.statusWithCode(666) -} \ No newline at end of file +} + +fun threadCount(namePattern: String) = Thread.getAllStackTraces().keys + .map { it.name } + .filter { it.contains(namePattern) } + .count()