diff --git a/README.md b/README.md
index ba22bc37e..e08853925 100644
--- a/README.md
+++ b/README.md
@@ -481,6 +481,13 @@ Module that covers the logging functionality using JBoss Logging Manager. The fo
- Inject a `Logger` instance using a custom category
- Setting up the log level property for logger instances
- Check default `quarkus.log.min-level` value
+-
+### `logging/thirdparty`
+
+Module that covers, that logging works with various third-party solutions. The following scenarios are covered:
+- Check default `quarkus.log.min-level` value
+- Syslog-type log (syslog-ng is used)
+- Option `quarkus.log.syslog.max-length` filters messages, which are too big
### `sql-db/hibernate`
@@ -865,7 +872,7 @@ Jaeger is deployed in an "all-in-one" configuration, and the OpenShift test veri
Testing OpenTelemetry with Jaeger components
- Extension `quarkus-opentelemetry` - responsible for traces generation in OpenTelemetry format and export into OpenTelemetry components (opentelemetry-agent, opentelemetry-collector)
-Scenarios that test proper traces export to Jaeger components, context propagation, OpenTelemetry SDK Autoconfiguration and CDI injection of OpenTelemetry beans.
+Scenarios that test proper traces export to Jaeger components, context propagation, OpenTelemetry SDK Autoconfiguration, OTLP Exporter proxy and CDI injection of OpenTelemetry beans.
See also `monitoring/opentelemetry/README.md`
### `micrometer/prometheus`
diff --git a/logging/thirdparty/pom.xml b/logging/thirdparty/pom.xml
new file mode 100644
index 000000000..88bb4f211
--- /dev/null
+++ b/logging/thirdparty/pom.xml
@@ -0,0 +1,19 @@
+
+
+ 4.0.0
+
+ io.quarkus.ts.qe
+ parent
+ 1.0.0-SNAPSHOT
+ ../..
+
+ logging-thridparty
+ jar
+ Quarkus QE TS: Logging: Third party
+
+
+ io.quarkus
+ quarkus-resteasy-reactive
+
+
+
diff --git a/logging/thirdparty/src/main/java/io/quarkus/ts/logging/jboss/LogResource.java b/logging/thirdparty/src/main/java/io/quarkus/ts/logging/jboss/LogResource.java
new file mode 100644
index 000000000..1e62e47d8
--- /dev/null
+++ b/logging/thirdparty/src/main/java/io/quarkus/ts/logging/jboss/LogResource.java
@@ -0,0 +1,59 @@
+package io.quarkus.ts.logging.jboss;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.QueryParam;
+
+import org.jboss.logging.Logger;
+
+import io.quarkus.arc.log.LoggerName;
+
+@Path("/log")
+public class LogResource {
+
+ public static final String CUSTOM_CATEGORY = "FOO";
+
+ private static final Logger LOG = Logger.getLogger(LogResource.class);
+
+ @Inject
+ Logger log;
+
+ @LoggerName(CUSTOM_CATEGORY)
+ Logger customCategoryLog;
+
+ @POST
+ @Path("/static/{level}")
+ public void addLogMessageInStaticLogger(@PathParam("level") String level, @QueryParam("message") String message) {
+ addLogMessage(LOG, level, message);
+ }
+
+ @POST
+ @Path("/field/{level}")
+ public void addLogMessageInFieldLogger(@PathParam("level") String level, @QueryParam("message") String message) {
+ addLogMessage(log, level, message);
+ }
+
+ @POST
+ @Path("/field-with-custom-category/{level}")
+ public void addLogMessageInFieldWithCustomCategoryLogger(@PathParam("level") String level,
+ @QueryParam("message") String message) {
+ addLogMessage(customCategoryLog, level, message);
+ }
+
+ @GET
+ public void logExample() {
+ LOG.fatal("Fatal log example");
+ LOG.error("Error log example");
+ LOG.warn("Warn log example");
+ LOG.info("Info log example");
+ LOG.debug("Debug log example");
+ LOG.trace("Trace log example");
+ }
+
+ private void addLogMessage(Logger logger, String level, String message) {
+ logger.log(Logger.Level.valueOf(level.toUpperCase()), message);
+ }
+}
diff --git a/logging/thirdparty/src/main/resources/application.properties b/logging/thirdparty/src/main/resources/application.properties
new file mode 100644
index 000000000..bfdd6662f
--- /dev/null
+++ b/logging/thirdparty/src/main/resources/application.properties
@@ -0,0 +1,18 @@
+#When you set the logging level below the minimum logging level, you must adjust the minimum logging level as well.
+#Otherwise, the value of minimum logging level overrides the logging level.
+# DOC:
+# https://access.redhat.com/documentation/en-us/red_hat_build_of_quarkus/1.11/html-single/configuring_logging_with_quarkus/index#ref-example-logging-configuration_quarkus-configuring-logging
+# https://quarkus.io/guides/logging
+# https://quarkus.io/guides/all-config#quarkus-core_quarkus.log.min-level
+
+# By default min-level is set to DEBUG
+#quarkus.log.min-level=DEBUG
+quarkus.log.level=INFO
+
+%syslog.quarkus.log.syslog.enable=true
+%syslog.quarkus.log.syslog.app-name=quarkus
+%syslog.quarkus.log.syslog.format=%-5p %s%n
+%syslog.quarkus.log.syslog.syslog-type=rfc3164
+%syslog.quarkus.log.syslog.protocol=tcp
+# the option below is overriden in @QuarkusScenario tests
+%syslog.quarkus.log.syslog.endpoint=localhost:1514
diff --git a/logging/thirdparty/src/test/java/io/quarkus/ts/logging/jboss/SyslogIT.java b/logging/thirdparty/src/test/java/io/quarkus/ts/logging/jboss/SyslogIT.java
new file mode 100644
index 000000000..6a01f569a
--- /dev/null
+++ b/logging/thirdparty/src/test/java/io/quarkus/ts/logging/jboss/SyslogIT.java
@@ -0,0 +1,66 @@
+package io.quarkus.ts.logging.jboss;
+
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+@QuarkusScenario
+public class SyslogIT {
+
+ /*
+ * For manual testing:
+ * podman run -p 8514:514 \
+ * -v $(pwd)/src/test/resources/syslog-ng.conf:/etc/syslog-ng/syslog-ng.conf:z --rm -it balabit/syslog-ng
+ */
+ @Container(image = "balabit/syslog-ng", port = 514, expectedLog = "syslog-ng")
+ static RestService syslog = new RestService()
+ .withProperty("_ignored", "resource_with_destination::/etc/syslog-ng/|syslog-ng.conf");
+
+ @QuarkusApplication
+ static RestService app = new RestService()
+ .withProperty("quarkus.profile", "syslog")
+ .withProperty("quarkus.log.syslog.max-length", "64")
+ .withProperty("quarkus.log.syslog.endpoint", () -> syslog.getURI().toString());
+
+ @Test
+ public void checkDefaultLogMinLevel() {
+ app.given().when().get("/log").then().statusCode(204);
+
+ syslog.logs().assertContains("Fatal log example");
+
+ syslog.logs().assertContains("Error log example");
+ syslog.logs().assertContains("Warn log example");
+ syslog.logs().assertContains("Info log example");
+
+ // the value of minimum logging level overrides the logging level
+ syslog.logs().assertDoesNotContain("Debug log example");
+ syslog.logs().assertDoesNotContain("Trace log example");
+ }
+
+ @Test
+ @Tag("https://issues.redhat.com/browse/QUARKUS-4531")
+ public void logBigMessage() {
+ String shorterMessage = "Relatively long message";
+ app.given().when()
+ .post("/log/static/info?message={message}", shorterMessage)
+ .then().statusCode(204);
+ syslog.logs().assertContains(shorterMessage);
+ }
+
+ @Test
+ @Tag("https://issues.redhat.com/browse/QUARKUS-4531")
+ public void filterBigMessage() {
+ String longerMessage = "Message, which is very long and is not expected to fit into 64 bytes";
+ app.given().when()
+ .post("/log/static/info?message={message}",
+ longerMessage)
+ .then().statusCode(204);
+
+ syslog.logs().assertDoesNotContain(longerMessage);
+ }
+
+}
diff --git a/logging/thirdparty/src/test/resources/syslog-ng.conf b/logging/thirdparty/src/test/resources/syslog-ng.conf
new file mode 100644
index 000000000..e23c3e6b7
--- /dev/null
+++ b/logging/thirdparty/src/test/resources/syslog-ng.conf
@@ -0,0 +1,11 @@
+@version: 4.4
+@include "scl.conf"
+
+log {
+ source {
+ #network();
+ tcp(ip(0.0.0.0) port(514));
+ };
+ #destination { file("/var/log/syslog"); };
+ destination {stdout();};
+};
\ No newline at end of file
diff --git a/monitoring/micrometer-prometheus/src/main/java/io/quarkus/ts/micrometer/prometheus/PathPatternResource.java b/monitoring/micrometer-prometheus/src/main/java/io/quarkus/ts/micrometer/prometheus/PathPatternResource.java
new file mode 100644
index 000000000..68ed21faa
--- /dev/null
+++ b/monitoring/micrometer-prometheus/src/main/java/io/quarkus/ts/micrometer/prometheus/PathPatternResource.java
@@ -0,0 +1,44 @@
+package io.quarkus.ts.micrometer.prometheus;
+
+import java.net.URI;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.core.Response;
+
+@Path("/test")
+public class PathPatternResource {
+ @GET
+ @Path("/redirect")
+ public Response redirect() {
+ return Response.status(Response.Status.FOUND) //302
+ .location(URI.create("/new-location"))
+ .build();
+ }
+
+ @GET
+ @Path("/not-found")
+ public Response notFound() {
+ return Response.status(Response.Status.NOT_FOUND).build(); //404
+ }
+
+ @GET
+ @Path("/not-found/{uri}")
+ public Response notFoundUri(@PathParam("uri") String uri) {
+ return Response.status(Response.Status.NOT_FOUND).build(); //404
+ }
+
+ @GET
+ @Path("/moved/{id}")
+ public Response moved(@PathParam("id") String id) {
+ return Response.status(Response.Status.MOVED_PERMANENTLY).build(); // 301
+ }
+
+ @GET
+ @Path("")
+ public Response emptyPath() {
+ return Response.status(Response.Status.NO_CONTENT).build(); //204
+ }
+
+}
diff --git a/monitoring/micrometer-prometheus/src/test/java/io/quarkus/ts/micrometer/prometheus/HttpPathMetricsPatternIT.java b/monitoring/micrometer-prometheus/src/test/java/io/quarkus/ts/micrometer/prometheus/HttpPathMetricsPatternIT.java
new file mode 100644
index 000000000..2af6fb5c0
--- /dev/null
+++ b/monitoring/micrometer-prometheus/src/test/java/io/quarkus/ts/micrometer/prometheus/HttpPathMetricsPatternIT.java
@@ -0,0 +1,113 @@
+package io.quarkus.ts.micrometer.prometheus;
+
+import static io.restassured.RestAssured.given;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.QuarkusApplication;
+
+@Tag("QUARKUS-4430")
+@QuarkusScenario
+public class HttpPathMetricsPatternIT {
+
+ private static final int ASSERT_METRICS_TIMEOUT_SECONDS = 30;
+ private static final List HTTP_SERVER_REQUESTS_METRICS_SUFFIX = Arrays.asList("count", "sum", "max");
+ private static final String HTTP_SERVER_REQUESTS_METRICS_FORMAT = "http_server_requests_seconds_%s{method=\"GET\",outcome=\"%s\",status=\"%d\",uri=\"%s\"}";
+ private static final String REDIRECT_ENDPOINT = "/test/redirect";
+ private static final String NOT_FOUND_ENDPOINT = "/test/not-found";
+ private static final String NOT_FOUND_URI_ENDPOINT = "/test/not-found/{uri}";
+ private static final String MOVED_ENDPOINT = "/test/moved/{id}";
+ private static final String EMPTY_ENDPOINT = "/test";
+
+ @QuarkusApplication
+ static RestService app = new RestService();
+
+ @Test
+ public void verifyNotFoundInMetrics() {
+ whenCallNotFoundEndpoint();
+ thenMetricIsExposedInServiceEndpoint(404, "CLIENT_ERROR", "NOT_FOUND");
+ }
+
+ @Test
+ public void verifyUriNotFoundInMetrics() {
+ whenCallNotFoundWithParamEndpoint("/url123");
+ thenMetricIsExposedInServiceEndpoint(404, "CLIENT_ERROR", NOT_FOUND_URI_ENDPOINT);
+ }
+
+ @Test
+ public void verifyRedirectionInMetrtics() {
+ whenCallRedirectEndpoint();
+ thenMetricIsExposedInServiceEndpoint(302, "REDIRECTION", "REDIRECTION");
+ }
+
+ @Test
+ public void verifyEmptyPathInMetrics() {
+ whenCallEmptyPathEndpoint();
+ thenMetricIsExposedInServiceEndpoint(204, "SUCCESS", EMPTY_ENDPOINT);
+ }
+
+ @Test
+ public void verifyDynamicRedirectionInMetrics() {
+ String id = "41";
+ whenCallDynamicSegmentEndpoint(id);
+ thenMetricIsExposedInServiceEndpoint(301, "REDIRECTION", MOVED_ENDPOINT);
+ }
+
+ private void whenCallRedirectEndpoint() {
+ given()
+ .redirects().follow(false)
+ .when().get(REDIRECT_ENDPOINT)
+ .then().statusCode(302);
+ }
+
+ private void whenCallEmptyPathEndpoint() {
+ given()
+ .when().get("/test")
+ .then().statusCode(HttpStatus.SC_NO_CONTENT);
+ }
+
+ private void whenCallNotFoundEndpoint() {
+ given()
+ .when().get(NOT_FOUND_ENDPOINT)
+ .then().statusCode(HttpStatus.SC_NOT_FOUND);
+ }
+
+ private void whenCallNotFoundWithParamEndpoint(String url) {
+ given()
+ .pathParam("uri", url)
+ .redirects().follow(false)
+ .when().get(NOT_FOUND_URI_ENDPOINT)
+ .then().statusCode(HttpStatus.SC_NOT_FOUND);
+ }
+
+ private void whenCallDynamicSegmentEndpoint(String id) {
+ given()
+ .pathParam("id", id)
+ .redirects().follow(false)
+ .when().get(MOVED_ENDPOINT)
+ .then().statusCode(HttpStatus.SC_MOVED_PERMANENTLY);
+ }
+
+ private void thenMetricIsExposedInServiceEndpoint(int statusCode, String outcome, String uri) {
+ await().ignoreExceptions().atMost(ASSERT_METRICS_TIMEOUT_SECONDS, TimeUnit.SECONDS).untilAsserted(() -> {
+ String metrics = app.given().get("/q/metrics").then()
+ .statusCode(HttpStatus.SC_OK).extract().asString();
+ for (String metricSuffix : HTTP_SERVER_REQUESTS_METRICS_SUFFIX) {
+ String metric = String.format(HTTP_SERVER_REQUESTS_METRICS_FORMAT, metricSuffix, outcome, statusCode, uri);
+
+ assertTrue(metrics.contains(metric), "Expected metric : " + metric + ". Metrics: " + metrics);
+ }
+ });
+ }
+
+}
diff --git a/monitoring/opentelemetry/pom.xml b/monitoring/opentelemetry/pom.xml
index b16d7977a..97ad52865 100644
--- a/monitoring/opentelemetry/pom.xml
+++ b/monitoring/opentelemetry/pom.xml
@@ -44,6 +44,21 @@
quarkus-test-service-jaeger
test
+
+ io.vertx
+ vertx-junit5
+ test
+
+
+ io.vertx
+ vertx-grpc-server
+ test
+
+
+ io.vertx
+ vertx-grpc-client
+ test
+
diff --git a/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/DevModeOpenTelemetryIT.java b/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/DevModeOpenTelemetryIT.java
new file mode 100644
index 000000000..def372732
--- /dev/null
+++ b/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/DevModeOpenTelemetryIT.java
@@ -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.debug("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 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();
+ }
+ });
+ }
+}
diff --git a/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryIT.java b/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryIT.java
index 7177126a8..44254ba85 100644
--- a/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryIT.java
+++ b/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryIT.java
@@ -51,7 +51,9 @@ public class OpenTelemetryIT {
.withProperty("quarkus.application.name", "pingservice")
.withProperty("pongservice_url", () -> pongservice.getURI(HTTP).getRestAssuredStyleUri())
.withProperty("pongservice_port", () -> Integer.toString(pongservice.getURI(HTTP).getPort()))
- .withProperty("quarkus.otel.exporter.otlp.traces.endpoint", jaeger::getCollectorUrl);
+ .withProperty("quarkus.otel.exporter.otlp.traces.endpoint", jaeger::getCollectorUrl)
+ // test exporter OTLP proxy is disabled by default
+ .withProperty("quarkus.otel.exporter.otlp.traces.proxy-options.host", "Host that must be ignored!");
@Order(1)
@Test
diff --git a/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryProxyIT.java b/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryProxyIT.java
new file mode 100644
index 000000000..7524824b2
--- /dev/null
+++ b/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryProxyIT.java
@@ -0,0 +1,163 @@
+package io.quarkus.ts.opentelemetry;
+
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Base64;
+
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.quarkus.test.bootstrap.JaegerService;
+import io.quarkus.test.bootstrap.Protocol;
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.JaegerContainer;
+import io.quarkus.test.services.QuarkusApplication;
+import io.quarkus.test.services.URILike;
+import io.quarkus.test.utils.AwaitilityUtils;
+import io.restassured.RestAssured;
+import io.restassured.response.Response;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpServer;
+import io.vertx.core.http.HttpServerOptions;
+import io.vertx.core.net.SocketAddress;
+import io.vertx.grpc.client.GrpcClient;
+import io.vertx.grpc.common.GrpcStatus;
+import io.vertx.grpc.server.GrpcServer;
+import io.vertx.grpc.server.GrpcServerRequest;
+import io.vertx.junit5.VertxExtension;
+
+@Tag("QUARKUS-4550")
+@ExtendWith(VertxExtension.class)
+@QuarkusScenario
+public class OpenTelemetryProxyIT {
+
+ private static final int PAGE_LIMIT = 10;
+ private static final String OPERATION_NAME = "GET /hello";
+ private static final String PROXY_AUTHORIZATION = "proxy-authorization";
+ private static final String BASIC_AUTH_PREFIX = "Basic ";
+ private static final String PROXY_USERNAME = "otel-user";
+ private static final String PROXY_PASSWORD = "otel-pwd";
+ private static final String JAEGER_MISSING_COLLECTOR = "missing.collector";
+
+ @JaegerContainer
+ static final JaegerService jaeger = new JaegerService();
+
+ @QuarkusApplication
+ static final RestService app = new RestService()
+ .withProperty("quarkus.otel.exporter.otlp.traces.proxy-options.enabled", "true")
+ .withProperty("quarkus.otel.exporter.otlp.traces.proxy-options.username", PROXY_USERNAME)
+ .withProperty("quarkus.otel.exporter.otlp.traces.proxy-options.password", PROXY_PASSWORD)
+ .withProperty("quarkus.otel.exporter.otlp.traces.proxy-options.port", OpenTelemetryProxyIT::getProxyPortAsString)
+ .withProperty("quarkus.otel.exporter.otlp.traces.proxy-options.host", "localhost")
+ .withProperty("quarkus.otel.exporter.otlp.traces.endpoint", "http://" + JAEGER_MISSING_COLLECTOR);
+
+ @Test
+ public void testProxyWithPassword(Vertx vertx) throws IOException {
+ try (var ignored = createGrpcProxy(vertx)) {
+ testTraces();
+ }
+ }
+
+ private static Closeable createGrpcProxy(Vertx vertx) {
+ GrpcServer grpcProxy = GrpcServer.server(vertx);
+
+ HttpServer proxyHttpServer = vertx.createHttpServer(new HttpServerOptions().setPort(getProxyPort()));
+ proxyHttpServer
+ .requestHandler(httpServerRequest -> {
+ assertEquals(JAEGER_MISSING_COLLECTOR, httpServerRequest.authority().host());
+ grpcProxy.handle(httpServerRequest);
+ })
+ .listen();
+
+ GrpcClient proxyClient = GrpcClient.client(vertx);
+
+ // create proxy server
+ // on request use gRPC client and pass the message to the Jaeger
+ grpcProxy.callHandler(reqFromQuarkus -> reqFromQuarkus.messageHandler(msgFromQuarkus -> proxyClient
+ .request(getJaegerSocketAddress())
+ .onSuccess(requestToJaeger -> {
+ assertProxyUsernameAndPassword(reqFromQuarkus);
+ requestToJaeger
+ .methodName(reqFromQuarkus.methodName())
+ .serviceName(reqFromQuarkus.serviceName())
+ .end(msgFromQuarkus.payload());
+ // send Jaeger response back to the Quarkus application
+ requestToJaeger.response()
+ .onSuccess(h -> h.messageHandler(msg -> reqFromQuarkus.response().endMessage(msg)))
+ .onFailure(err -> reqFromQuarkus.response().status(GrpcStatus.ABORTED).end());
+ })));
+ return () -> proxyHttpServer
+ .close()
+ .eventually(proxyClient::close)
+ .toCompletionStage()
+ .toCompletableFuture()
+ .join();
+ }
+
+ private static void assertProxyUsernameAndPassword(GrpcServerRequest reqFromQuarkus) {
+ var proxyAuthZ = reqFromQuarkus.headers().get(PROXY_AUTHORIZATION);
+ if (proxyAuthZ != null && proxyAuthZ.startsWith(BASIC_AUTH_PREFIX)) {
+ var basicCredentials = new String(Base64.getDecoder().decode(proxyAuthZ.substring(BASIC_AUTH_PREFIX.length())));
+ assertEquals(PROXY_USERNAME + ":" + PROXY_PASSWORD, basicCredentials);
+ return;
+ }
+ Assertions.fail(
+ "OpenTelemetry OTLP exporter is not passing proxy authorization, found headers: " + reqFromQuarkus.headers());
+ }
+
+ private static void testTraces() {
+ doRequest();
+ assertTraces();
+ }
+
+ private static void assertTraces() {
+ AwaitilityUtils.untilAsserted(() -> thenRetrieveTraces()
+ .then()
+ .statusCode(200)
+ .body("data.spans.flatten().findAll { it.operationName == '%s' }.size()".formatted(OPERATION_NAME),
+ greaterThan(0)));
+ }
+
+ private static Response thenRetrieveTraces() {
+ return RestAssured
+ .given()
+ .queryParam("operation", OPERATION_NAME)
+ .queryParam("lookback", "1h")
+ .queryParam("limit", PAGE_LIMIT)
+ .queryParam("service", "pingpong")
+ .get(jaeger.getTraceUrl());
+ }
+
+ private static void doRequest() {
+ app.given()
+ .get("/hello")
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .body(is("pong"));
+ }
+
+ private static URILike getJaegerUri() {
+ return jaeger.getURI(Protocol.NONE);
+ }
+
+ private static int getProxyPort() {
+ return getJaegerUri().getPort() + 1;
+ }
+
+ private static String getProxyPortAsString() {
+ return Integer.toString(getProxyPort());
+ }
+
+ private static SocketAddress getJaegerSocketAddress() {
+ return SocketAddress.inetSocketAddress(getJaegerUri().getPort(), getJaegerUri().getHost());
+ }
+}
diff --git a/pom.xml b/pom.xml
index d225bc6df..c26ba3292 100644
--- a/pom.xml
+++ b/pom.xml
@@ -433,6 +433,7 @@
super-size/many-extensions
quarkus-cli
logging/jboss
+ logging/thirdparty
qute/multimodule
qute/synchronous
qute/reactive