feat(tracing): add quarkus-tracing-service module

This commit is contained in:
Jorge Bornhausen 2025-06-18 01:40:26 +02:00
parent 75a778296c
commit db0026b723
Signed by: jorge.bornhausen
SSH key fingerprint: SHA256:X2ootOwvCeP4FoNfmVUFIKIbhq95tAgnt7Oqg3x+lfs
20 changed files with 867 additions and 0 deletions

View file

@ -11,6 +11,7 @@
<module>quarkus-json-service</module>
<module>quarkus-message-digest-service</module>
<module>quarkus-random-number-generator</module>
<module>quarkus-tracing-service</module>
<module>quarkus-uuid-generator</module>
</modules>

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ch.phoenix.oss</groupId>
<artifactId>quarkus-commons</artifactId>
<version>1.0.1-SNAPSHOT</version>
</parent>
<artifactId>quarkus-tracing-service</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-config-yaml</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,10 @@
package ch.phoenix.oss.quarkus.commons.tracing;
import org.eclipse.microprofile.config.spi.Converter;
public class LowerCaseStringConverter implements Converter<String> {
@Override
public String convert(String value) throws IllegalArgumentException, NullPointerException {
return value.toLowerCase();
}
}

View file

@ -0,0 +1,40 @@
package ch.phoenix.oss.quarkus.commons.tracing;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithConverter;
import io.smallrye.config.WithDefault;
import java.util.Optional;
import java.util.Set;
@ConfigMapping(prefix = "phoenix.commons.tracing")
public interface TracingConfiguration {
RequestFilterConfiguration requestFilter();
interface RequestFilterConfiguration {
Headers headers();
interface Headers {
Optional<Set<@WithConverter(LowerCaseStringConverter.class) String>> redact();
}
Path path();
interface Path {
@WithDefault("false")
boolean includeRaw();
}
Query query();
interface Query {
@WithDefault("false")
boolean includeRaw();
Optional<Set<@WithConverter(LowerCaseStringConverter.class) String>> redact();
}
}
}

View file

@ -0,0 +1,18 @@
package ch.phoenix.oss.quarkus.commons.tracing;
public class TracingConstants {
public static final String ACTOR = "actor";
public static final String ANONYMOUS = "anonymous";
public static final String REQUEST_ROUTE = "request.route";
public static final String REQUEST_METHOD = "request.method";
public static final String REQUEST_PATH_RAW = "request.path.raw";
public static final String REQUEST_PATH_PARAMS = "request.path.params";
public static final String REQUEST_QUERY_RAW = "request.query.raw";
public static final String REQUEST_QUERY_PARAMS = "request.query.params";
public static final String REQUEST_HEADERS = "request.headers";
public static final String REQUEST_CLIENT_IP = "request.client.ip";
public static final String SCHEDULER_JOB_NAME = "scheduler.job.name";
private TracingConstants() {}
}

View file

@ -0,0 +1,139 @@
package ch.phoenix.oss.quarkus.commons.tracing;
import io.quarkus.arc.Unremovable;
import io.quarkus.logging.Log;
import io.quarkus.security.identity.SecurityIdentity;
import io.vertx.ext.web.RoutingContext;
import jakarta.inject.Inject;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ResourceInfo;
import java.util.List;
import java.util.Set;
import org.jboss.resteasy.reactive.server.ServerRequestFilter;
@Unremovable
public class TracingRequestFilter {
private final RoutingContext routingContext;
private final TracingService tracingService;
private final SecurityIdentity securityIdentity;
private final TracingConfiguration configuration;
@Inject
public TracingRequestFilter(
RoutingContext routingContext,
TracingService tracingService,
SecurityIdentity securityIdentity,
TracingConfiguration configuration) {
this.routingContext = routingContext;
this.tracingService = tracingService;
this.securityIdentity = securityIdentity;
this.configuration = configuration;
}
@ServerRequestFilter
public void filter(ContainerRequestContext requestContext, ResourceInfo resourceInfo) {
if (securityIdentity.isAnonymous()) {
tracingService.trace(TracingConstants.ACTOR, TracingConstants.ANONYMOUS);
} else {
tracingService.trace(
TracingConstants.ACTOR, securityIdentity.getPrincipal().getName());
}
var method = requestContext.getMethod();
tracingService.trace(TracingConstants.REQUEST_METHOD, method);
var routePattern = getRoutePattern(resourceInfo);
tracingService.trace(TracingConstants.REQUEST_ROUTE, routePattern);
var uriInfo = requestContext.getUriInfo();
uriInfo.getPathParameters()
.forEach((key, value) ->
tracingService.trace(TracingConstants.REQUEST_PATH_PARAMS + '.' + key, joinStrings(value)));
if (configuration.requestFilter().path().includeRaw()) {
tracingService.trace(
TracingConstants.REQUEST_PATH_RAW, uriInfo.getAbsolutePath().getRawPath());
}
requestContext.getHeaders().forEach((key, value) -> {
var lowerCaseKey = key.toLowerCase();
var property = TracingConstants.REQUEST_HEADERS + '.' + lowerCaseKey;
if (configuration
.requestFilter()
.headers()
.redact()
.orElse(Set.of())
.contains(lowerCaseKey)) {
tracingService.trace(property, "********");
} else {
tracingService.trace(property, joinStrings(value));
}
});
uriInfo.getQueryParameters()
.forEach((key, value) ->
tracingService.trace(TracingConstants.REQUEST_QUERY_PARAMS + '.' + key, joinStrings(value)));
if (configuration.requestFilter().query().includeRaw()) {
var rawQuery = uriInfo.getRequestUri().getRawQuery();
if (rawQuery != null && !rawQuery.isBlank()) {
tracingService.trace(TracingConstants.REQUEST_QUERY_RAW, rawQuery);
}
}
tracingService.trace(
TracingConstants.REQUEST_CLIENT_IP,
routingContext.request().connection().remoteAddress(true).hostAddress());
if (Log.isTraceEnabled()) {
Log.tracef(
"Incoming request: %s %s", method, uriInfo.getAbsolutePath().getRawPath());
}
}
private static String joinStrings(List<String> value) {
return String.join(", ", value);
}
private String getRoutePattern(ResourceInfo resourceInfo) {
String classPath = getPathValue(resourceInfo.getResourceClass().getAnnotation(Path.class));
String methodPath = getPathValue(resourceInfo.getResourceMethod().getAnnotation(Path.class));
if (!classPath.isEmpty()) {
if (methodPath.isEmpty()) {
return "/" + classPath;
} else {
return "/" + classPath + "/" + methodPath;
}
} else {
if (methodPath.isEmpty()) {
return "/";
} else {
return "/" + methodPath;
}
}
}
private static String getPathValue(Path path) {
if (path == null) {
return "";
}
return trimSlashes(path.value());
}
private static String trimSlashes(String segment) {
if (segment.isEmpty() || "/".equals(segment)) {
return "";
}
// Assuming that it's not possible for a segment to contain //,
// thus it's possible to avoid extra ifs and whiles, as well
// as the use of regexes
int start = (segment.charAt(0) == '/') ? 1 : 0;
int end = (segment.charAt(segment.length() - 1) == '/') ? segment.length() - 1 : segment.length();
return segment.substring(start, end);
}
}

View file

@ -0,0 +1,24 @@
package ch.phoenix.oss.quarkus.commons.tracing;
public interface TracingService {
void clearAll();
void trace(String key, Object value);
String getActor();
String getRequestPath();
String getRequestMethod();
String getRequestId();
String getTraceId();
String getSpanId();
String getClientIp();
String getSchedulerJob();
}

View file

@ -0,0 +1,69 @@
package ch.phoenix.oss.quarkus.commons.tracing;
import io.opentelemetry.api.trace.Span;
import io.quarkus.arc.DefaultBean;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.MDC;
@DefaultBean
@ApplicationScoped
class TracingServiceImpl implements TracingService {
private final Span span;
TracingServiceImpl(Span span) {
this.span = span;
}
@Override
public void clearAll() {
MDC.clear();
}
@Override
public void trace(final String key, final Object value) {
Log.infof("tracing key=%s value=%s", key, value);
MDC.put(key, value);
}
@Override
public String getActor() {
return (String) MDC.get(TracingConstants.ACTOR);
}
@Override
public String getRequestPath() {
return (String) MDC.get(TracingConstants.REQUEST_PATH_RAW);
}
@Override
public String getRequestMethod() {
return (String) MDC.get(TracingConstants.REQUEST_METHOD);
}
@Override
public String getRequestId() {
return (String) MDC.get(TracingConstants.REQUEST_HEADERS + ".x-request-id");
}
@Override
public String getTraceId() {
return span.getSpanContext().getTraceId();
}
@Override
public String getSpanId() {
return span.getSpanContext().getSpanId();
}
@Override
public String getClientIp() {
return (String) MDC.get(TracingConstants.REQUEST_CLIENT_IP);
}
@Override
public String getSchedulerJob() {
return (String) MDC.get(TracingConstants.SCHEDULER_JOB_NAME);
}
}

View file

@ -0,0 +1,44 @@
package ch.phoenix.oss.quarkus.commons.tracing;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectSpy;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
@QuarkusTest
public class ActorTest {
@InjectSpy
TracingService tracingService;
@Test
void getAuthenticated() {
var route = "/authenticated";
RestAssured.given()
.auth()
.basic("jon", "doe")
.accept(ContentType.TEXT)
.when()
.get(route)
.then()
.statusCode(200);
verify(tracingService).trace("actor", "jon");
verify(tracingService).trace("request.method", "GET");
verify(tracingService).trace("request.route", route);
verify(tracingService).trace("request.headers.accept", "text/plain");
verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate");
verify(tracingService).trace("request.headers.authorization", "Basic am9uOmRvZQ==");
verify(tracingService).trace("request.headers.connection", "Keep-Alive");
verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:"));
verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient"));
verify(tracingService).trace("request.client.ip", "127.0.0.1");
verifyNoMoreInteractions(tracingService);
}
}

View file

@ -0,0 +1,46 @@
package ch.phoenix.oss.quarkus.commons.tracing;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import io.quarkus.test.junit.mockito.InjectSpy;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
@QuarkusTest
@TestProfile(Test2Profile.class)
public class RawPathTest {
@InjectSpy
TracingService tracingService;
@Test
void getWithRawPath() {
var route = "/no-leading-and-trailing/{param}/{param2}";
RestAssured.given()
.accept(ContentType.TEXT)
.when()
.get(route, 1, 2)
.then()
.statusCode(200);
verify(tracingService).trace("actor", "anonymous");
verify(tracingService).trace("request.method", "GET");
verify(tracingService).trace("request.route", route);
verify(tracingService).trace("request.path.params.param", "1");
verify(tracingService).trace("request.path.params.param2", "2");
verify(tracingService).trace("request.path.raw", "/no-leading-and-trailing/1/2");
verify(tracingService).trace("request.headers.accept", "text/plain");
verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate");
verify(tracingService).trace("request.headers.connection", "Keep-Alive");
verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:"));
verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient"));
verify(tracingService).trace("request.client.ip", "127.0.0.1");
verifyNoMoreInteractions(tracingService);
}
}

View file

@ -0,0 +1,204 @@
package ch.phoenix.oss.quarkus.commons.tracing;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectSpy;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import java.util.Map;
import org.junit.jupiter.api.Test;
@QuarkusTest
class RoutePatternTest {
@InjectSpy
TracingService tracingService;
@Test
void getBlankResource() {
var route = "/";
RestAssured.given().accept(ContentType.TEXT).when().get(route).then().statusCode(200);
verifyGetTracing(route, Map.of());
}
@Test
void postBlankResource() {
var route = "/";
RestAssured.given().accept(ContentType.TEXT).when().post(route).then().statusCode(200);
verify(tracingService).trace("actor", "anonymous");
verify(tracingService).trace("request.method", "POST");
verify(tracingService).trace("request.route", route);
verify(tracingService).trace("request.headers.accept", "text/plain");
verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate");
verify(tracingService).trace("request.headers.connection", "Keep-Alive");
verify(tracingService).trace("request.headers.content-length", "0");
verify(tracingService)
.trace("request.headers.content-type", "application/x-www-form-urlencoded; charset=ISO-8859-1");
verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:"));
verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient"));
verify(tracingService).trace("request.client.ip", "127.0.0.1");
verifyNoMoreInteractions(tracingService);
}
@Test
void getLeadingResource() {
var route = "/leading/{id}/{anotherId}";
RestAssured.given()
.accept(ContentType.TEXT)
.when()
.get(route, 1, 2)
.then()
.statusCode(200);
verifyGetTracing(route, Map.of("id", "1", "anotherId", "2"));
}
@Test
void getTrailingResource() {
var route = "/{id}/{anotherId}/trailing";
RestAssured.given()
.accept(ContentType.TEXT)
.when()
.get(route, 1, 2)
.then()
.statusCode(200);
verifyGetTracing(route, Map.of("id", "1", "anotherId", "2"));
}
@Test
void getLeadingAndNoTrailingResource() {
var route = "/leading-and-no-trailing";
RestAssured.given().accept(ContentType.TEXT).when().get(route).then().statusCode(200);
verifyGetTracing(route, Map.of());
}
@Test
void getLeadingAndNoTrailingWithSingleParamResource() {
var route = "/leading-and-no-trailing/{param}";
RestAssured.given().accept(ContentType.TEXT).when().get(route, 1).then().statusCode(200);
verifyGetTracing(route, Map.of("param", "1"));
}
@Test
void getLeadingAndNoTrailingWithMultiParamResource() {
var route = "/leading-and-no-trailing/{param}/{param2}";
RestAssured.given()
.accept(ContentType.TEXT)
.when()
.get(route, 1, 2)
.then()
.statusCode(200);
verifyGetTracing(route, Map.of("param", "1", "param2", "2"));
}
@Test
void getLeadingAndTrailingResource() {
var route = "/leading-and-trailing";
RestAssured.given().accept(ContentType.TEXT).when().get(route).then().statusCode(200);
verifyGetTracing(route, Map.of());
}
@Test
void getLeadingAndTrailingWithSingleParamResource() {
var route = "/leading-and-trailing/{param}";
RestAssured.given().accept(ContentType.TEXT).when().get(route, 1).then().statusCode(200);
verifyGetTracing(route, Map.of("param", "1"));
}
@Test
void getLeadingAndTrailingWithMultiParamResource() {
var route = "/leading-and-trailing/{param}/{param2}";
RestAssured.given()
.accept(ContentType.TEXT)
.when()
.get(route, 1, 2)
.then()
.statusCode(200);
verifyGetTracing(route, Map.of("param", "1", "param2", "2"));
}
@Test
void getNoLeadingAndNoTrailingResource() {
var route = "/no-leading-and-no-trailing";
RestAssured.given().accept(ContentType.TEXT).when().get(route).then().statusCode(200);
verifyGetTracing(route, Map.of());
}
@Test
void geNoLeadingAndNoTrailingWithSingleParamResource() {
var route = "/no-leading-and-no-trailing/{param}";
RestAssured.given().accept(ContentType.TEXT).when().get(route, 1).then().statusCode(200);
verifyGetTracing(route, Map.of("param", "1"));
}
@Test
void getNoLeadingAndNoTrailingWithMultiParamResource() {
var route = "/no-leading-and-no-trailing/{param}/{param2}";
RestAssured.given()
.accept(ContentType.TEXT)
.when()
.get(route, 1, 2)
.then()
.statusCode(200);
verifyGetTracing(route, Map.of("param", "1", "param2", "2"));
}
@Test
void getNoLeadingAndTrailingResource() {
var route = "/no-leading-and-trailing";
RestAssured.given().accept(ContentType.TEXT).when().get(route).then().statusCode(200);
verifyGetTracing(route, Map.of());
}
@Test
void getNoLeadingAndTrailingWithSingleParamResource() {
var route = "/no-leading-and-trailing/{param}";
RestAssured.given().accept(ContentType.TEXT).when().get(route, 1).then().statusCode(200);
verifyGetTracing(route, Map.of("param", "1"));
}
@Test
void getNoLeadingAndTrailingWithMultiParamResource() {
var route = "/no-leading-and-trailing/{param}/{param2}";
RestAssured.given()
.accept(ContentType.TEXT)
.when()
.get(route, 1, 2)
.then()
.statusCode(200);
verifyGetTracing(route, Map.of("param", "1", "param2", "2"));
}
private void verifyGetTracing(String route, Map<String, String> pathParams) {
verify(tracingService).trace("actor", "anonymous");
verify(tracingService).trace("request.method", "GET");
verify(tracingService).trace("request.route", route);
pathParams.forEach((key, value) -> verify(tracingService).trace("request.path.params." + key, value));
verify(tracingService).trace("request.headers.accept", "text/plain");
verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate");
verify(tracingService).trace("request.headers.connection", "Keep-Alive");
verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:"));
verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient"));
verify(tracingService).trace("request.client.ip", "127.0.0.1");
verifyNoMoreInteractions(tracingService);
}
}

View file

@ -0,0 +1,11 @@
package ch.phoenix.oss.quarkus.commons.tracing;
import io.quarkus.test.junit.QuarkusTestProfile;
public class Test2Profile implements QuarkusTestProfile {
@Override
public String getConfigProfile() {
return "test2";
}
}

View file

@ -0,0 +1,15 @@
package ch.phoenix.oss.quarkus.commons.tracing.resource;
import io.quarkus.security.Authenticated;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Authenticated
@Path("authenticated")
public class AuthenticatedResource {
@GET
public String get() {
return "Hello user";
}
}

View file

@ -0,0 +1,25 @@
package ch.phoenix.oss.quarkus.commons.tracing.resource;
import jakarta.annotation.security.PermitAll;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("")
@Produces(MediaType.TEXT_PLAIN)
public class BlankResource {
@GET
@PermitAll
public String get() {
return "get";
}
@POST
@Path("")
public String post() {
return "post";
}
}

View file

@ -0,0 +1,29 @@
package ch.phoenix.oss.quarkus.commons.tracing.resource;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/leading-and-no-trailing")
@Produces(MediaType.TEXT_PLAIN)
public class LeadingAndNoTrailingResource {
@GET
@Path("")
public String root() {
return "root";
}
@GET
@Path("/{param}")
public String singleParam(String param) {
return param;
}
@GET
@Path("/{param}/{param2}")
public String multiParam(String param, String param2) {
return param + "/" + param2;
}
}

View file

@ -0,0 +1,29 @@
package ch.phoenix.oss.quarkus.commons.tracing.resource;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/leading-and-trailing/")
@Produces(MediaType.TEXT_PLAIN)
public class LeadingAndTrailingResource {
@GET
@Path("/")
public String root() {
return "root";
}
@GET
@Path("/{param}/")
public String singleParam(String param) {
return param;
}
@GET
@Path("/{param}/{param2}/")
public String multiParam(String param, String param2) {
return param + "/" + param2;
}
}

View file

@ -0,0 +1,29 @@
package ch.phoenix.oss.quarkus.commons.tracing.resource;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("no-leading-and-no-trailing")
@Produces(MediaType.TEXT_PLAIN)
public class NoLeadingAndNoTrailingResource {
@GET
@Path("")
public String root() {
return "root";
}
@GET
@Path("{param}")
public String singleParam(String param) {
return param;
}
@GET
@Path("{param}/{param2}")
public String multiParam(String param, String param2) {
return param + "/" + param2;
}
}

View file

@ -0,0 +1,29 @@
package ch.phoenix.oss.quarkus.commons.tracing.resource;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("no-leading-and-trailing/")
@Produces(MediaType.TEXT_PLAIN)
public class NoLeadingAndTrailingResource {
@GET
@Path("/")
public String root() {
return "root";
}
@GET
@Path("{param}/")
public String singleParam(String param) {
return param;
}
@GET
@Path("{param}/{param2}/")
public String multiParam(String param, String param2) {
return param + "/" + param2;
}
}

View file

@ -0,0 +1,23 @@
package ch.phoenix.oss.quarkus.commons.tracing.resource;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/")
@Produces(MediaType.TEXT_PLAIN)
public class SlashResource {
@GET
@Path("/leading/{id}/{anotherId}")
public String doubleLeading(int id, int anotherId) {
return "leading/" + id + "/" + anotherId;
}
@GET
@Path("{id}/{anotherId}/trailing/")
public String doubleTrailing(int id, int anotherId) {
return id + "/" + anotherId + "/trailing";
}
}

View file

@ -0,0 +1,38 @@
quarkus:
http:
test-port: 0
log:
category:
"org.apache.http.wire":
level: DEBUG # set to DEBUG when RestAssured logs are necessary to understand test failures
otel:
traces:
exporter: none
security:
users:
embedded:
enabled: true
plain-text: true
users:
jon: doe
"%test2":
quarkus:
log:
min-level: TRACE
category:
"ch.phoenix.oss.quarkus.commons.tracing":
level: TRACE
phoenix:
commons:
tracing:
request-filter:
path:
include-raw: true
headers:
redact:
- AUTHORIZATION
query:
include-raw: true
redact:
- ACCESS_TOKEN