Skip to content

Commit

Permalink
Add Vertx openTelemetry scenario
Browse files Browse the repository at this point in the history
- replace resteasy endpoints by reactiveRoutes
- add opentelemetry and opentracing dependencies
- add opentelemetry test case
  • Loading branch information
pablo gonzalez granados committed Apr 14, 2021
1 parent eb57207 commit b60761e
Show file tree
Hide file tree
Showing 13 changed files with 230 additions and 115 deletions.
24 changes: 18 additions & 6 deletions 300-quarkus-vertx-webClient/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-mutiny</artifactId>
<artifactId>quarkus-opentelemetry-exporter-jaeger</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
Expand All @@ -32,13 +32,25 @@
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-web-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
package io.quarkus.qe.vertx.webclient;
package io.quarkus.qe.vertx.webclient.handler;

import io.quarkus.qe.vertx.webclient.model.Joke;
import io.quarkus.qe.vertx.webclient.config.ChuckEndpointValue;
import io.quarkus.qe.vertx.webclient.config.VertxWebClientConfig;
import io.quarkus.vertx.web.Route;
import io.quarkus.vertx.web.RouteBase;
import io.smallrye.mutiny.Uni;
import io.vertx.core.json.Json;
import io.vertx.mutiny.core.Vertx;
import io.vertx.mutiny.ext.web.client.WebClient;
import io.vertx.mutiny.ext.web.client.predicate.ResponsePredicate;
import io.vertx.mutiny.ext.web.codec.BodyCodec;
import java.net.HttpURLConnection;
import org.eclipse.microprofile.config.inject.ConfigProperty;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;

@Path("/chuck")

import static io.quarkus.vertx.web.Route.HttpMethod;

@RouteBase(path = "/chuck")
public class ChuckNorrisResource {

@Inject
Expand All @@ -41,51 +42,41 @@ void initialize() {
this.client = WebClient.create(vertx);
}

@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/")
public Uni<Response> getRandomJoke() {
@Route(methods = HttpMethod.GET, path = "/")
public Uni<Joke> getRandomJoke() {
return getChuckQuoteAsJoke()
.map(resp -> Response.ok(resp).build())
.ifNoItem().after(Duration.ofSeconds(httpClientConf.timeout)).fail()
.onFailure().retry().atMost(httpClientConf.retries);
}

@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/bodyCodec")
public Uni<Response> getRandomJokeWithBodyCodec() {
@Route(methods = HttpMethod.GET, path = "/bodyCodec", produces = "application/json")
public Uni<Joke> getRandomJokeWithBodyCodec() {
return client.getAbs(chuckNorrisQuote.getValue())
.as(BodyCodec.json(Joke.class))
.putHeader("Accept", "application/json")
.expect(ResponsePredicate.status(Response.Status.OK.getStatusCode()))
.expect(ResponsePredicate.status(HttpURLConnection.HTTP_OK))
.send()
.map(resp -> Response.ok(resp.body()).build())
.map(resp -> resp.body())
.ifNoItem().after(Duration.ofSeconds(httpClientConf.timeout)).fail()
.onFailure().retry().atMost(httpClientConf.retries);
}

@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/combine")
public Uni<Response> getTwoRandomJokes() {
@Route(methods = HttpMethod.GET, path = "/combine", produces = "application/json")
public Uni<List<Joke>> getTwoRandomJokes() {
Uni<Joke> jokeOne = getChuckQuoteAsJoke();
Uni<Joke> jokeTwo = getChuckQuoteAsJoke();

return Uni.combine()
.all()
.unis(jokeOne, jokeTwo)
.combinedWith((BiFunction<Joke, Joke, List<Joke>>) Arrays::asList)
.map(resp -> Response.ok(Json.encode(resp)).build());
.combinedWith((BiFunction<Joke, Joke, List<Joke>>) Arrays::asList);
}

private Uni<Joke> getChuckQuoteAsJoke() {
return client.getAbs(chuckNorrisQuote.getValue())
.putHeader("Accept", "application/json")
.expect(ResponsePredicate.status(Response.Status.OK.getStatusCode()))
.expect(ResponsePredicate.status(HttpURLConnection.HTTP_OK))
.send()
.map(resp -> resp.bodyAsJsonObject().mapTo(Joke.class));
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.qe.vertx.webclient.handler;

import io.quarkus.vertx.web.Route;
import io.smallrye.mutiny.TimeoutException;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonObject;
import java.net.HttpURLConnection;
import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class FailureHandler {
@Route(path = "/*", type = Route.HandlerType.FAILURE, produces = "application/json")
void runtimeFailures(RuntimeException e, HttpServerResponse response) {
if(e instanceof TimeoutException){
response.setStatusCode(HttpURLConnection.HTTP_CLIENT_TIMEOUT).end(Json.encode(new JsonObject().put("msg", e.getMessage())));
}else{
response.setStatusCode(HttpURLConnection.HTTP_INTERNAL_ERROR).end(Json.encode(new JsonObject().put("msg", e.getMessage())));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.quarkus.qe.vertx.webclient.handler;

import io.quarkus.vertx.web.Route;
import io.quarkus.vertx.web.RouteBase;

@RouteBase(path = "/trace")
public class TracingExampleResource {
@Route(methods = Route.HttpMethod.GET, path = "/hello")
boolean validateRequestSinglePara() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.quarkus.qe.vertx.webclient;
package io.quarkus.qe.vertx.webclient.model;


import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Configuration file
quarkus.http.port=8081
chucknorris.api.domain=https://api.chucknorris.io
vertx.webclient.timeout-sec=2
vertx.webclient.retries=3
vertx.webclient.retries=3

# Jaeger
quarkus.opentelemetry.tracer.exporter.jaeger.endpoint=http://localhost:14250/api/traces
Original file line number Diff line number Diff line change
@@ -1,94 +1,127 @@
package io.quarkus.qe.vertx.webclient;

import io.quarkus.qe.vertx.webclient.resources.JaegerTestResource;
import io.quarkus.qe.vertx.webclient.resources.WireMockChuckNorrisResource;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import java.net.HttpURLConnection;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.http.HttpStatus;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import javax.ws.rs.core.Response;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static io.restassured.RestAssured.given;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.containsStringIgnoringCase;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.core.Every.everyItem;
import static org.hamcrest.core.StringContains.containsString;

@QuarkusTest
@QuarkusTestResource(WireMockChuckNorrisResource.class)
@QuarkusTestResource(JaegerTestResource.class)
public class ChuckNorrisResourceTest {

final static String EXPECTED_ID = "aBanNLDwR-SAz7iMHuCiyw";
final static String EXPECTED_VALUE = "Chuck Norris has already been to mars; that why there's no signs of life";
private final static String jaegerEndpoint = "http://localhost:16686/api/traces";
static final String EXPECTED_ID = "aBanNLDwR-SAz7iMHuCiyw";
static final String EXPECTED_VALUE = "Chuck Norris has already been to mars; that why there's no signs of life";
static final int DELAY = 3500; // must be greater than vertx.webclient.timeout-sec
static final String QUARKUS_PROFILE = "quarkus.profile";
static final String NATIVE = "native";
static final boolean IS_NATIVE = System.getProperty(QUARKUS_PROFILE, "").equals(NATIVE);

@Test
@DisplayName("Vert.x WebClient [flavor: mutiny] -> Map json response body to POJO")
public void getChuckJokeAsJSON(){

stubFor(get(urlEqualTo("/jokes/random"))
.willReturn(aResponse()
.withHeader("Accept", "application/json")
.withBody(String.format("{\"categories\":[]," +
"\"created_at\":\"2020-01-05 13:42:19.576875\"," +
"\"icon_url\":\"https://assets.chucknorris.host/img/avatar/chuck-norris.png\"," +
"\"id\":\"%s\"," +
"\"updated_at\":\"2020-01-05 13:42:19.576875\"," +
"\"url\":\"https://api.chucknorris.io/jokes/sC09X1xQQymE4SciIjyV0g\"," +
"\"value\":\"%s\"}", EXPECTED_ID, EXPECTED_VALUE))));

setupMockHttpServer();
given()
.when()
.get("/chuck")
.get("/chuck/")
.then()
.statusCode(Response.Status.OK.getStatusCode())
.body(containsStringIgnoringCase((String.format("{\"id\":\"%s\",\"jokeText\":\"%s\"}", EXPECTED_ID, EXPECTED_VALUE))));
.statusCode(HttpURLConnection.HTTP_OK)
.body("id", containsStringIgnoringCase(EXPECTED_ID))
.body("value", containsStringIgnoringCase(EXPECTED_VALUE));
}

@Test
@DisplayName("Vert.x WebClient [flavor: mutiny] -> Mapped json response by 'as' mutiny method.")
public void getChuckJokeByJsonBodyCodec() throws InterruptedException {
setupMockHttpServer();
given()
.when()
.get("/chuck/bodyCodec/")
.then()
.statusCode(HttpURLConnection.HTTP_OK)
.body("id", containsStringIgnoringCase(EXPECTED_ID))
.body("value", containsStringIgnoringCase(EXPECTED_VALUE));
}

@Test
@DisplayName("Vert.x WebClient [flavor: mutiny] -> If third party server exceed http client timeout, then throw a timeout exception.")
public void getTimeoutWhenResponseItsTooSlow(){
stubFor(get(urlEqualTo("/jokes/random"))
.willReturn(aResponse()
.withHeader("Accept", "application/json")
.withBody(String.format("{\"categories\":[]," +
"\"created_at\":\"2020-01-05 13:42:19.576875\"," +
"\"icon_url\":\"https://assets.chucknorris.host/img/avatar/chuck-norris.png\"," +
"\"id\":\"%s\"," +
"\"updated_at\":\"2020-01-05 13:42:19.576875\"," +
"\"url\":\"https://api.chucknorris.io/jokes/sC09X1xQQymE4SciIjyV0g\"," +
"\"value\":\"%s\"}", EXPECTED_ID, EXPECTED_VALUE))));
.withFixedDelay(DELAY)));

given()
.filter(
(request, response, ctx) -> {
io.restassured.response.Response resp = ctx.next(request, response);
if (resp.statusCode() >= 400) {
System.err.println(resp.body().prettyPrint());
System.err.println(request.getMethod() + " " + request.getURI() + " => "
+ response.getStatusCode() + " " + response.getStatusLine());
}
return resp;
})
.when()
.get("/chuck/bodyCodec")
.get("/chuck/bodyCodec/")
.then()
.statusCode(Response.Status.OK.getStatusCode())
.body(containsStringIgnoringCase((String.format("{\"id\":\"%s\",\"jokeText\":\"%s\"}", EXPECTED_ID, EXPECTED_VALUE))));
.statusCode(HttpURLConnection.HTTP_CLIENT_TIMEOUT);
}

@Test
@DisplayName("Vert.x WebClient [flavor: mutiny] -> If third party server exceed http client timeout, then throw a timeout exception.")
public void getTimeoutWhenResponseItsTooSlow(){
final int delay = 3500; // must be greater than vertx.webclient.timeout-sec
public void endpointShouldTrace() {
final int pageLimit = 50;
final String expectedOperationName = "trace/hello";
await().atMost(1, TimeUnit.MINUTES).pollInterval(Duration.ofSeconds(1)).untilAsserted(() -> {
given().when()
.get("/trace/hello")
.then()
.statusCode(HttpStatus.SC_OK);

List<String> operationNames = given().when()
.queryParam("limit", pageLimit)
.queryParam("lookback", "1h")
.queryParam("service", getServiceName())
.queryParam("operation", expectedOperationName)
.get(jaegerEndpoint)
.then()
.statusCode(HttpStatus.SC_OK)
.body("data.size()", greaterThan(0))
.body("data.spans", hasSize(greaterThan(0)))
.body("data.spans.tags", hasSize(greaterThan(0)))
.body("data.spans.operationName", not(empty()))
.extract().jsonPath().getList("data.spans.operationName", String.class);

assertThat(operationNames, everyItem(containsString(expectedOperationName)));
});
}

private void setupMockHttpServer() {
stubFor(get(urlEqualTo("/jokes/random"))
.willReturn(aResponse()
.withHeader("Accept", "application/json")
.withFixedDelay(delay)));
.withBody(String.format("{\"categories\":[]," +
"\"created_at\":\"2020-01-05 13:42:19.576875\"," +
"\"icon_url\":\"https://assets.chucknorris.host/img/avatar/chuck-norris.png\"," +
"\"id\":\"%s\"," +
"\"updated_at\":\"2020-01-05 13:42:19.576875\"," +
"\"url\":\"https://api.chucknorris.io/jokes/sC09X1xQQymE4SciIjyV0g\"," +
"\"value\":\"%s\"}", EXPECTED_ID, EXPECTED_VALUE))));
}

given()
.when()
.get("/chuck/bodyCodec")
.then()
.statusCode(Response.Status.REQUEST_TIMEOUT.getStatusCode());
private String getServiceName(){
return (IS_NATIVE)?"300-quarkus-vertx-webclient":"<<unset>>".toLowerCase();
}
}
Loading

0 comments on commit b60761e

Please sign in to comment.