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