Skip to content

Commit

Permalink
add some opentelemetry coverage tests (#1749)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcarranzan authored Apr 28, 2024
1 parent 7b12a56 commit a1ec8a3
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 11 deletions.
9 changes: 9 additions & 0 deletions messaging/amqp-reactive/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,19 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus.qe</groupId>
<artifactId>quarkus-test-service-amq</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus.qe</groupId>
<artifactId>quarkus-test-service-jaeger</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -1,20 +1,81 @@
package io.quarkus.ts.messaging.amqpreactive;

import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.is;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

import org.hamcrest.Matcher;
import org.junit.jupiter.api.Test;

import io.quarkus.test.bootstrap.AmqService;
import io.quarkus.test.bootstrap.JaegerService;
import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.services.AmqContainer;
import io.quarkus.test.services.JaegerContainer;
import io.quarkus.test.services.QuarkusApplication;
import io.quarkus.test.services.containers.model.AmqProtocol;
import io.restassured.response.Response;

@QuarkusScenario
public class ProdAmqpReactiveIT extends BaseAmqpReactiveIT {

@JaegerContainer(expectedLog = "\"Health Check state change\",\"status\":\"ready\"")
static final JaegerService jaeger = new JaegerService();

@AmqContainer(image = "${amqbroker.image}", protocol = AmqProtocol.AMQP)
static AmqService amq = new AmqService();

@QuarkusApplication
static RestService app = new RestService()
.withProperty("amqp-host", amq::getAmqpHost)
.withProperty("amqp-port", () -> "" + amq.getPort());
.withProperty("amqp-port", () -> "" + amq.getPort())
.withProperty("quarkus.otel.exporter.otlp.traces.endpoint", jaeger::getCollectorUrl);

private Response resp;

@Test
public void testContextPropagation() {
int pageLimit = 10;
String operationName = "GET /price";
String lookback = "1h";
String serviceName = "messaging-amqp-reactive";
await().atMost(5, TimeUnit.SECONDS).pollInterval(Duration.ofSeconds(1)).untilAsserted(() -> {
thenRetrieveTraces(operationName, lookback, pageLimit, serviceName);
thenTraceSpanSizeMustBe(is(1));
verifyStandardSourceCodeAttributesArePresent(operationName);

});
}

private void thenRetrieveTraces(String operationName, String lookback, int pageLimit, String serviceName) {
resp = app.given().when()
.queryParam("operation", operationName)
.queryParam("lookback", lookback)
.queryParam("limit", pageLimit)
.queryParam("service", serviceName)
.get(jaeger.getTraceUrl())
.then().log().all().extract().response();
}

private void thenTraceSpanSizeMustBe(Matcher<?> matcher) {
resp.then().body("data[0].spans.size()", matcher);
}

private void verifyStandardSourceCodeAttributesArePresent(String operationName) {
verifyAttributeValue(operationName, "code.namespace", PriceResource.class.getName());
verifyAttributeValue(operationName, "code.function", "price");
}

private void verifyAttributeValue(String operationName, String attributeName, String attributeValue) {
resp.then().body(getGPathForOperationAndAttribute(operationName, attributeName), is(attributeValue));
}

private static String getGPathForOperationAndAttribute(String operationName, String attribute) {
return String.format("data[0].spans.find { it.operationName == '%s' }.tags.find { it.key == '%s' }.value",
operationName, attribute);
}

}
4 changes: 4 additions & 0 deletions monitoring/opentelemetry-reactive/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-scheduler</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus.qe</groupId>
<artifactId>quarkus-test-service-jaeger</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.ts.opentelemetry.reactive;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

@Path("/admin")
@RolesAllowed("admin")
public class AdminResource {

@GET
public String get(@Context SecurityContext security) {
return "Hello, admin " + security.getUserPrincipal().getName();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,14 @@ quarkus.grpc.clients.pong.host=localhost
quarkus.grpc.clients.pong.port=${quarkus.http.port}
quarkus.grpc.server.use-separate-server=false

quarkus.application.name=pingpong
quarkus.application.name=pingpong

# basic authentication
quarkus.http.auth.basic=true
quarkus.security.users.embedded.enabled=true
quarkus.security.users.embedded.plain-text=true
quarkus.security.users.embedded.users.alice=alice
quarkus.security.users.embedded.roles.alice=admin

# Export security events
quarkus.otel.security-events.enabled=true
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import static io.quarkus.test.bootstrap.Protocol.HTTP;
import static io.restassured.RestAssured.given;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.text.IsEqualIgnoringCase.equalToIgnoringCase;
Expand Down Expand Up @@ -31,6 +34,9 @@ public class OpentelemetryReactiveIT {

private Response resp;

static final String ADMIN_USERNAME = "alice";
static final String ADMIN_PASSWORD = "alice";

@JaegerContainer(useOtlpCollector = true, expectedLog = "\"Health Check state change\",\"status\":\"ready\"")
static final JaegerService jaeger = new JaegerService();

Expand All @@ -40,7 +46,7 @@ public class OpentelemetryReactiveIT {
.withProperty("quarkus.application.name", "pongservice")
.withProperty("quarkus.otel.exporter.otlp.traces.endpoint", jaeger::getCollectorUrl);

@QuarkusApplication(classes = { PingResource.class, PingPongService.class })
@QuarkusApplication(classes = { PingResource.class, PingPongService.class, AdminResource.class })
static final RestService pingservice = new RestService()
.withProperty("pongservice_url", () -> pongservice.getURI(HTTP).getRestAssuredStyleUri())
.withProperty("pongservice_port", () -> Integer.toString(pongservice.getURI(HTTP).getPort()))
Expand Down Expand Up @@ -70,6 +76,19 @@ public void testContextPropagation() {
});
}

@Test
public void testSecurityEvents() {
int pageLimit = 10;
String serviceName = "pingservice";
String operationName = "GET /admin";
await().atMost(30, TimeUnit.SECONDS).pollInterval(Duration.ofSeconds(1)).untilAsserted(() -> {
doSecurityEndpointRequest();
thenRetrieveTraces(pageLimit, "1h", serviceName, operationName);
assertSecurityEventsAndLogsPresent();
});

}

@Test
public void testSchedulerTracing() {
// FIXME: report breaking change if not fixed: https://github.com/quarkusio/quarkus/pull/35989#issuecomment-1836023316
Expand Down Expand Up @@ -101,13 +120,33 @@ public void whenDoPingPongRequest() {
.statusCode(HttpStatus.SC_OK).body(equalToIgnoringCase("ping pong"));
}

private void doSecurityEndpointRequest() {
given()
.auth().basic(ADMIN_USERNAME, ADMIN_PASSWORD)
.get(pingservice.getURI(HTTP).withPath("/admin").toString())
.then()
.statusCode(HttpStatus.SC_OK)
.body(equalTo("Hello, admin " + ADMIN_USERNAME));
}

private void thenRetrieveTraces(int pageLimit, String lookBack, String serviceName, String operationName) {
resp = given().when()
.queryParam("operation", operationName)
.queryParam("lookback", lookBack)
.queryParam("limit", pageLimit)
.queryParam("service", serviceName)
.get(jaeger.getTraceUrl());
await().atMost(10, TimeUnit.SECONDS)
.pollInterval(Duration.ofSeconds(1))
.until(() -> {
resp = given().when()
.queryParam("operation", operationName)
.queryParam("lookback", lookBack)
.queryParam("limit", pageLimit)
.queryParam("service", serviceName)
.get(jaeger.getTraceUrl());
return !resp.jsonPath().getList("data.spans").isEmpty();
});
}

private void assertSecurityEventsAndLogsPresent() {
resp.then().body(
"data[0].spans.findAll { span -> span.operationName == 'GET /admin' }[0].logs.flatten().findAll { log -> log.fields.find { field -> field.key == 'event' && field.value == 'quarkus.security.authorization.success' } }",
is(not(empty())));
}

private void thenTraceSpanSizeMustBe(Matcher<?> matcher) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ quarkus.grpc.clients.pong.host=localhost
quarkus.grpc.clients.pong.port=${quarkus.http.port}
quarkus.grpc.server.use-separate-server=false

quarkus.application.name=pingpong
quarkus.application.name=pingpong
quarkus.otel.traces.enabled=true
quarkus.log.level=INFO
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package io.quarkus.ts.opentelemetry;

import static io.quarkus.test.utils.AwaitilityUtils.untilAsserted;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;

import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import org.apache.http.HttpStatus;
import org.jboss.logging.Logger;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import io.quarkus.test.bootstrap.DevModeQuarkusService;
import io.quarkus.test.bootstrap.JaegerService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.services.DevModeQuarkusApplication;
import io.quarkus.test.services.JaegerContainer;
import io.restassured.response.Response;

@QuarkusScenario
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class DevModeOpenTelemetryIT {

private static final Logger LOG = Logger.getLogger(DevModeOpenTelemetryIT.class);
private static final String APPLICATION_PROPERTIES = Paths.get("src", "main", "resources", "application.properties")
.toFile()
.getPath();
private static final String TRACES_ENABLE_PROPERTY = "quarkus.otel.traces.enabled=";

private static final int PAGE_LIMIT = 10;
private static volatile String previousLiveReloadLogEntry = null;

@JaegerContainer(expectedLog = "\"Health Check state change\",\"status\":\"ready\"")
static final JaegerService jaeger = new JaegerService();

@DevModeQuarkusApplication(classes = { PingPongService.class, PingResource.class,
PongResource.class })
static DevModeQuarkusService otel = (DevModeQuarkusService) new DevModeQuarkusService()
.withProperty("quarkus.otel.exporter.otlp.traces.endpoint", jaeger::getCollectorUrl);

@Test
@Order(2)
void checkTraces() {
String operationName = "GET /hello";
modifyAppPropertiesAndWait(props -> props.replace(getOtelEnabledProperty(false), getOtelEnabledProperty(true)));
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
doRequest();
Response response = thenRetrieveTraces(PAGE_LIMIT, "1h", "pingpong", operationName);
response.then()
.body("data.size()", greaterThan(0));
});
}

@Test
@Order(1)
void checkThereIsNoTracesAfterRestart() {
String operationName = "GET /hello";
modifyAppPropertiesAndWait(props -> props.replace(getOtelEnabledProperty(true), getOtelEnabledProperty(false)));

await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
doRequest();
Response response = thenRetrieveTraces(PAGE_LIMIT, "1h", "pingpong", operationName);
response.then()
.body("data", empty());
});
}

private Response thenRetrieveTraces(int pageLimit, String lookBack, String serviceName, String operationName) {
Response response = otel.given()
.when()
.queryParam("operation", operationName)
.queryParam("lookback", lookBack)
.queryParam("limit", pageLimit)
.queryParam("service", serviceName)
.get(jaeger.getTraceUrl());
LOG.info("Traces --> " + response.asPrettyString());
return response;
}

private static void doRequest() {
otel.given()
.get("/hello")
.then()
.statusCode(HttpStatus.SC_OK)
.body(is("pong"));
}

private static String getOtelEnabledProperty(boolean enabled) {
return TRACES_ENABLE_PROPERTY + enabled;
}

private static void modifyAppPropertiesAndWait(Function<String, String> transformProperties) {
otel.modifyFile(APPLICATION_PROPERTIES, transformProperties);
untilAsserted(() -> {
// just waiting won't do the trick, we need to ping Quarkus as well
doRequest();
String logEntry = otel
.getLogs()
.stream()
.filter(entry -> entry.contains("Live reload total time")
&& (previousLiveReloadLogEntry == null || !previousLiveReloadLogEntry.equals(entry)))
.findAny()
.orElse(null);
if (logEntry != null) {
previousLiveReloadLogEntry = logEntry;
} else {
Assertions.fail();
}
});
}
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
<quarkus.platform.version>999-SNAPSHOT</quarkus.platform.version>
<quarkus.ide.version>3.10.0</quarkus.ide.version>
<quarkus.qe.framework.version>1.4.2.Beta10</quarkus.qe.framework.version>
<quarkus.qe.framework.version>1.4.2.Beta11</quarkus.qe.framework.version>
<quarkus-qpid-jms.version>2.6.1</quarkus-qpid-jms.version>
<apache-httpclient-fluent.version>4.5.14</apache-httpclient-fluent.version>
<confluent.kafka-avro-serializer.version>7.5.1</confluent.kafka-avro-serializer.version>
Expand Down

0 comments on commit a1ec8a3

Please sign in to comment.