@@ -193,7 +196,7 @@
@{project.version}
mvnw
chore: release @{releaseLabel}
- chore: prepare for next development iteration [skip ci]
+ chore: prepare for next development iteration
true
diff --git a/quarkus-audit-tools/pom.xml b/quarkus-audit-tools/pom.xml
new file mode 100644
index 0000000..1a01d07
--- /dev/null
+++ b/quarkus-audit-tools/pom.xml
@@ -0,0 +1,88 @@
+
+
+ 4.0.0
+
+
+ ch.phoenix.oss
+ quarkus-commons
+ 1.0.9-SNAPSHOT
+
+
+ quarkus-audit-tools
+ jar
+
+
+
+ ch.phoenix.oss
+ quarkus-tracing-service
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-hibernate-envers
+
+
+ io.quarkus
+ quarkus-hibernate-orm-panache
+
+
+ io.quarkus
+ quarkus-jdbc-postgresql
+
+
+ io.quarkus
+ quarkus-flyway
+ test
+
+
+ io.quarkus
+ quarkus-flyway-postgresql
+ test
+
+
+ io.quarkus
+ quarkus-elytron-security-properties-file
+ test
+
+
+ io.quarkus
+ quarkus-config-yaml
+ test
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco-plugin.version}
+
+
+ jacoco-check
+
+ check
+
+ test
+
+ ${project.build.directory}/jacoco-quarkus.exec
+
+
+ BUNDLE
+
+
+ INSTRUCTION
+ COVEREDRATIO
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditRevisionListener.java b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditRevisionListener.java
new file mode 100644
index 0000000..29884f8
--- /dev/null
+++ b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditRevisionListener.java
@@ -0,0 +1,20 @@
+package ch.phoenix.oss.quarkus.commons.audit;
+
+import jakarta.enterprise.inject.spi.CDI;
+import org.hibernate.envers.RevisionListener;
+
+public class AuditRevisionListener implements RevisionListener {
+
+ @Override
+ public void newRevision(Object revisionEntity) {
+ var provider = CDI.current().select(RevisionContextProvider.class).get();
+
+ var rev = (Revision) revisionEntity;
+ rev.actor = provider.getActor();
+ rev.traceId = provider.getTraceId();
+ rev.spanId = provider.getSpanId();
+ rev.requestId = provider.getRequestId();
+ rev.clientIp = provider.getClientIp();
+ rev.hostName = provider.getHostName();
+ }
+}
diff --git a/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntity.java b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntity.java
new file mode 100644
index 0000000..4540cbf
--- /dev/null
+++ b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntity.java
@@ -0,0 +1,30 @@
+package ch.phoenix.oss.quarkus.commons.audit;
+
+import io.quarkus.hibernate.orm.panache.PanacheEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.MappedSuperclass;
+import java.time.Instant;
+import org.hibernate.envers.NotAudited;
+
+/**
+ * The goal of this class is to have on the entity itself the exact same
+ * timestamps as the ones from revisions generated by envers. Because of that,
+ * we can't use @CreationTimestamp and @UpdateTimestamp, as those timestamp values
+ * are managed by different Hibernate classes, so the generated values will drift.
+ *
+ * Manually setting these values to match envers revisions would be error-prone,
+ * verbose and tedious. So, the recommendation is to implement triggers on the
+ * audit tables which will update the main entity whenever a revision is created.
+ * An example of how to do that can be found in this module's integration tests.
+ */
+@MappedSuperclass
+public abstract class AuditedPanacheEntity extends PanacheEntity {
+
+ @NotAudited
+ @Column(updatable = false)
+ public Instant createdAt;
+
+ @NotAudited
+ @Column(updatable = false)
+ public Instant lastUpdatedAt;
+}
diff --git a/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityBase.java b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityBase.java
new file mode 100644
index 0000000..a6114b3
--- /dev/null
+++ b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityBase.java
@@ -0,0 +1,30 @@
+package ch.phoenix.oss.quarkus.commons.audit;
+
+import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
+import jakarta.persistence.Column;
+import jakarta.persistence.MappedSuperclass;
+import java.time.Instant;
+import org.hibernate.envers.NotAudited;
+
+/**
+ * The goal of this class is to have on the entity itself the exact same
+ * timestamps as the ones from revisions generated by envers. Because of that,
+ * we can't use @CreationTimestamp and @UpdateTimestamp, as those timestamp values
+ * are managed by different Hibernate classes, so the generated values will drift.
+ *
+ * Manually setting these values to match envers revisions would be error-prone,
+ * verbose and tedious. So, the recommendation is to implement triggers on the
+ * audit tables which will update the main entity whenever a revision is created.
+ * An example of how to do that can be found in this module's integration tests.
+ */
+@MappedSuperclass
+public abstract class AuditedPanacheEntityBase extends PanacheEntityBase {
+
+ @NotAudited
+ @Column(updatable = false)
+ public Instant createdAt;
+
+ @NotAudited
+ @Column(updatable = false)
+ public Instant lastUpdatedAt;
+}
diff --git a/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProvider.java b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProvider.java
new file mode 100644
index 0000000..18ebba6
--- /dev/null
+++ b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProvider.java
@@ -0,0 +1,67 @@
+package ch.phoenix.oss.quarkus.commons.audit;
+
+import ch.phoenix.oss.quarkus.commons.tracing.TracingService;
+import io.opentelemetry.instrumentation.annotations.WithSpan;
+import io.quarkus.arc.DefaultBean;
+import io.quarkus.arc.Unremovable;
+import io.quarkus.logging.Log;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+@Unremovable
+@DefaultBean
+@ApplicationScoped
+class DefaultRevisionContextProvider implements RevisionContextProvider {
+
+ private static final String UNKNOWN = "unknown";
+
+ private final TracingService tracingService;
+
+ @Inject
+ DefaultRevisionContextProvider(TracingService tracingService) {
+ this.tracingService = tracingService;
+ }
+
+ @Override
+ @WithSpan
+ public String getActor() {
+ return tracingService.getActor();
+ }
+
+ @Override
+ @WithSpan
+ public String getTraceId() {
+ return tracingService.getTraceId();
+ }
+
+ @Override
+ @WithSpan
+ public String getSpanId() {
+ return tracingService.getSpanId();
+ }
+
+ @Override
+ @WithSpan
+ public String getRequestId() {
+ return tracingService.getRequestId();
+ }
+
+ @Override
+ @WithSpan
+ public String getClientIp() {
+ return tracingService.getClientIp();
+ }
+
+ @Override
+ @WithSpan
+ public String getHostName() {
+ try {
+ return InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ Log.error("Unable to determine host name", e);
+ return UNKNOWN;
+ }
+ }
+}
diff --git a/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/Revision.java b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/Revision.java
new file mode 100644
index 0000000..fa3085a
--- /dev/null
+++ b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/Revision.java
@@ -0,0 +1,58 @@
+package ch.phoenix.oss.quarkus.commons.audit;
+
+import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
+import jakarta.persistence.*;
+import java.time.Instant;
+import java.util.Objects;
+import org.hibernate.envers.RevisionEntity;
+import org.hibernate.envers.RevisionNumber;
+import org.hibernate.envers.RevisionTimestamp;
+
+@Entity
+@Table(name = "revinfo")
+@RevisionEntity(AuditRevisionListener.class)
+public class Revision extends PanacheEntityBase {
+
+ @Id
+ @GeneratedValue
+ @RevisionNumber
+ public long rev;
+
+ @RevisionTimestamp
+ @Column(nullable = false, updatable = false)
+ public Instant timestamp;
+
+ @Column(updatable = false)
+ public String actor;
+
+ @Column(updatable = false)
+ public String traceId;
+
+ @Column(updatable = false)
+ public String spanId;
+
+ @Column(updatable = false)
+ public String requestId;
+
+ @Column(updatable = false)
+ public String clientIp;
+
+ @Column(updatable = false)
+ public String hostName;
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Revision that)) return false;
+ return rev == that.rev;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(rev);
+ }
+
+ @Override
+ public String toString() {
+ return "Revision{rev=" + rev + '}';
+ }
+}
diff --git a/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/RevisionContextProvider.java b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/RevisionContextProvider.java
new file mode 100644
index 0000000..477c44b
--- /dev/null
+++ b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/RevisionContextProvider.java
@@ -0,0 +1,16 @@
+package ch.phoenix.oss.quarkus.commons.audit;
+
+public interface RevisionContextProvider {
+
+ String getActor();
+
+ String getTraceId();
+
+ String getSpanId();
+
+ String getRequestId();
+
+ String getClientIp();
+
+ String getHostName();
+}
diff --git a/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityBaseTest.java b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityBaseTest.java
new file mode 100644
index 0000000..f4ba0de
--- /dev/null
+++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityBaseTest.java
@@ -0,0 +1,136 @@
+package ch.phoenix.oss.quarkus.commons.audit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.mockito.Mockito.when;
+
+import ch.phoenix.oss.quarkus.commons.tracing.TracingService;
+import io.quarkus.narayana.jta.QuarkusTransaction;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.mockito.InjectSpy;
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import java.time.Instant;
+import org.hibernate.envers.AuditReaderFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class AuditedPanacheEntityBaseTest {
+
+ @Inject
+ EntityManager entityManager;
+
+ @InjectSpy
+ TracingService tracingService;
+
+ @SuppressWarnings("Convert2MethodRef")
+ @BeforeEach
+ void setup() {
+ QuarkusTransaction.requiringNew().run(() -> TestEntity2.deleteAll());
+ }
+
+ @Test
+ void persistAndUpdate() {
+ when(tracingService.getRequestId()).thenReturn("00000000-0000-0000-0000-000000000000");
+ when(tracingService.getSpanId()).thenReturn("0000000000000000");
+ when(tracingService.getTraceId()).thenReturn("00000000000000000000000000000000");
+ when(tracingService.getActor()).thenReturn("unknown");
+ when(tracingService.getClientIp()).thenReturn("unknown");
+
+ var now = Instant.now();
+
+ QuarkusTransaction.requiringNew().run(() -> {
+ var entity = new TestEntity2("something");
+ entity.persist();
+ });
+
+ QuarkusTransaction.requiringNew().run(() -> {
+ var entity = TestEntity2.findBySomething("something");
+ assertAll(
+ () -> assertThat(entity.createdAt)
+ .as("createdAt should be after or equal to expected value")
+ .isAfterOrEqualTo(now),
+ () -> assertThat(entity.lastUpdatedAt)
+ .as("lastUpdatedAt should be equal to createdAt")
+ .isEqualTo(entity.createdAt));
+
+ var auditReader = AuditReaderFactory.get(entityManager);
+
+ var revisions = auditReader.getRevisions(TestEntity2.class, entity.id);
+ assertThat(revisions).hasSize(1);
+
+ var revInfo = entityManager.find(Revision.class, revisions.getFirst());
+ assertThat(revInfo).isNotNull();
+
+ assertAll(
+ () -> assertThat(revInfo.timestamp)
+ .as("revision timestamp should be equal to entity's createdAt timestamp")
+ .isEqualTo(entity.createdAt),
+ () -> assertThat(revInfo.actor)
+ .as("actor should match expected value")
+ .isEqualTo("unknown"),
+ () -> assertThat(revInfo.traceId)
+ .as("traceId should match expected value")
+ .isEqualTo("00000000000000000000000000000000"),
+ () -> assertThat(revInfo.spanId)
+ .as("spanId should match expected value")
+ .isEqualTo("0000000000000000"),
+ () -> assertThat(revInfo.requestId)
+ .as("requestId should match expected value")
+ .isEqualTo("00000000-0000-0000-0000-000000000000"),
+ () -> assertThat(revInfo.clientIp)
+ .as("clientIp should match expected value")
+ .isEqualTo("unknown"),
+ () -> assertThat(revInfo.hostName)
+ .as("hostName should not be blank")
+ .isNotBlank());
+ });
+
+ QuarkusTransaction.requiringNew().run(() -> {
+ var entity = TestEntity2.findBySomething("something");
+ entity.something = "else";
+ });
+
+ QuarkusTransaction.requiringNew().run(() -> {
+ var entity = TestEntity2.findBySomething("else");
+ assertAll(() -> assertThat(entity.createdAt)
+ .as("createdAt should be before lastUpdatedAt")
+ .isBefore(entity.lastUpdatedAt));
+
+ var auditReader = AuditReaderFactory.get(entityManager);
+
+ var revisions = auditReader.getRevisions(TestEntity2.class, entity.id);
+ assertThat(revisions).hasSize(2);
+
+ Revision revInfo = Revision.findById(revisions.getLast());
+ assertThat(revInfo).isNotNull();
+
+ assertAll(
+ () -> assertThat(revInfo.timestamp)
+ .as("revision timestamp should not be equal to entity's createdAt")
+ .isNotEqualTo(entity.createdAt),
+ () -> assertThat(revInfo.timestamp)
+ .as("revision timestamp should be equal to entity's lastUpdatedAt")
+ .isEqualTo(entity.lastUpdatedAt),
+ () -> assertThat(revInfo.actor)
+ .as("actor should match expected value")
+ .isEqualTo("unknown"),
+ () -> assertThat(revInfo.traceId)
+ .as("traceId should match expected value")
+ .isEqualTo("00000000000000000000000000000000"),
+ () -> assertThat(revInfo.spanId)
+ .as("spanId should match expected value")
+ .isEqualTo("0000000000000000"),
+ () -> assertThat(revInfo.requestId)
+ .as("requestId should match expected value")
+ .isEqualTo("00000000-0000-0000-0000-000000000000"),
+ () -> assertThat(revInfo.clientIp)
+ .as("clientIp should match expected value")
+ .isEqualTo("unknown"),
+ () -> assertThat(revInfo.hostName)
+ .as("hostName should not be blank")
+ .isNotBlank());
+ });
+ }
+}
diff --git a/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityTest.java b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityTest.java
new file mode 100644
index 0000000..0f182ee
--- /dev/null
+++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityTest.java
@@ -0,0 +1,136 @@
+package ch.phoenix.oss.quarkus.commons.audit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.mockito.Mockito.when;
+
+import ch.phoenix.oss.quarkus.commons.tracing.TracingService;
+import io.quarkus.narayana.jta.QuarkusTransaction;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.mockito.InjectSpy;
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import java.time.Instant;
+import org.hibernate.envers.AuditReaderFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class AuditedPanacheEntityTest {
+
+ @Inject
+ EntityManager entityManager;
+
+ @InjectSpy
+ TracingService tracingService;
+
+ @SuppressWarnings("Convert2MethodRef")
+ @BeforeEach
+ void setup() {
+ QuarkusTransaction.requiringNew().run(() -> TestEntity.deleteAll());
+ }
+
+ @Test
+ void persistAndUpdate() {
+ when(tracingService.getRequestId()).thenReturn("00000000-0000-0000-0000-000000000000");
+ when(tracingService.getSpanId()).thenReturn("0000000000000000");
+ when(tracingService.getTraceId()).thenReturn("00000000000000000000000000000000");
+ when(tracingService.getActor()).thenReturn("unknown");
+ when(tracingService.getClientIp()).thenReturn("unknown");
+
+ var now = Instant.now();
+
+ QuarkusTransaction.requiringNew().run(() -> {
+ var entity = new TestEntity("something");
+ entity.persist();
+ });
+
+ QuarkusTransaction.requiringNew().run(() -> {
+ var entity = TestEntity.findBySomething("something");
+ assertAll(
+ () -> assertThat(entity.createdAt)
+ .as("createdAt should be after or equal to expected value")
+ .isAfterOrEqualTo(now),
+ () -> assertThat(entity.lastUpdatedAt)
+ .as("lastUpdatedAt should be equal to createdAt")
+ .isEqualTo(entity.createdAt));
+
+ var auditReader = AuditReaderFactory.get(entityManager);
+
+ var revisions = auditReader.getRevisions(TestEntity.class, entity.id);
+ assertThat(revisions).hasSize(1);
+
+ var revInfo = entityManager.find(Revision.class, revisions.getFirst());
+ assertThat(revInfo).isNotNull();
+
+ assertAll(
+ () -> assertThat(revInfo.timestamp)
+ .as("revision timestamp should be equal to entity's createdAt timestamp")
+ .isEqualTo(entity.createdAt),
+ () -> assertThat(revInfo.actor)
+ .as("actor should match expected value")
+ .isEqualTo("unknown"),
+ () -> assertThat(revInfo.traceId)
+ .as("traceId should match expected value")
+ .isEqualTo("00000000000000000000000000000000"),
+ () -> assertThat(revInfo.spanId)
+ .as("spanId should match expected value")
+ .isEqualTo("0000000000000000"),
+ () -> assertThat(revInfo.requestId)
+ .as("requestId should match expected value")
+ .isEqualTo("00000000-0000-0000-0000-000000000000"),
+ () -> assertThat(revInfo.clientIp)
+ .as("clientIp should match expected value")
+ .isEqualTo("unknown"),
+ () -> assertThat(revInfo.hostName)
+ .as("hostName should not be blank")
+ .isNotBlank());
+ });
+
+ QuarkusTransaction.requiringNew().run(() -> {
+ var entity = TestEntity.findBySomething("something");
+ entity.something = "else";
+ });
+
+ QuarkusTransaction.requiringNew().run(() -> {
+ var entity = TestEntity.findBySomething("else");
+ assertAll(() -> assertThat(entity.createdAt)
+ .as("createdAt should be before lastUpdatedAt")
+ .isBefore(entity.lastUpdatedAt));
+
+ var auditReader = AuditReaderFactory.get(entityManager);
+
+ var revisions = auditReader.getRevisions(TestEntity.class, entity.id);
+ assertThat(revisions).hasSize(2);
+
+ Revision revInfo = Revision.findById(revisions.getLast());
+ assertThat(revInfo).isNotNull();
+
+ assertAll(
+ () -> assertThat(revInfo.timestamp)
+ .as("revision timestamp should not be equal to entity's createdAt")
+ .isNotEqualTo(entity.createdAt),
+ () -> assertThat(revInfo.timestamp)
+ .as("revision timestamp should be equal to entity's lastUpdatedAt")
+ .isEqualTo(entity.lastUpdatedAt),
+ () -> assertThat(revInfo.actor)
+ .as("actor should match expected value")
+ .isEqualTo("unknown"),
+ () -> assertThat(revInfo.traceId)
+ .as("traceId should match expected value")
+ .isEqualTo("00000000000000000000000000000000"),
+ () -> assertThat(revInfo.spanId)
+ .as("spanId should match expected value")
+ .isEqualTo("0000000000000000"),
+ () -> assertThat(revInfo.requestId)
+ .as("requestId should match expected value")
+ .isEqualTo("00000000-0000-0000-0000-000000000000"),
+ () -> assertThat(revInfo.clientIp)
+ .as("clientIp should match expected value")
+ .isEqualTo("unknown"),
+ () -> assertThat(revInfo.hostName)
+ .as("hostName should not be blank")
+ .isNotBlank());
+ });
+ }
+}
diff --git a/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProviderTest.java b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProviderTest.java
new file mode 100644
index 0000000..8ef3a73
--- /dev/null
+++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProviderTest.java
@@ -0,0 +1,31 @@
+package ch.phoenix.oss.quarkus.commons.audit;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.mockito.Mockito.mockStatic;
+
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class DefaultRevisionContextProviderTest {
+
+ @Inject
+ DefaultRevisionContextProvider underTest;
+
+ @Test
+ void getHostName() {
+ assertThat(underTest.getHostName()).isNotBlank();
+ }
+
+ @Test
+ void getHostNameWhenUnknown() {
+ try (var inetMock = mockStatic(InetAddress.class)) {
+ inetMock.when(InetAddress::getLocalHost).thenThrow(new UnknownHostException("simulated failure"));
+
+ assertThat(underTest.getHostName()).isEqualTo("unknown");
+ }
+ }
+}
diff --git a/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/RevisionTest.java b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/RevisionTest.java
new file mode 100644
index 0000000..d0f5d1f
--- /dev/null
+++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/RevisionTest.java
@@ -0,0 +1,51 @@
+package ch.phoenix.oss.quarkus.commons.audit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.quarkus.test.junit.QuarkusTest;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class RevisionTest {
+
+ @Test
+ void testEquals() {
+ var r1 = new Revision();
+ r1.rev = 1;
+
+ var r2 = new Revision();
+ r2.rev = 1;
+
+ var r3 = new Revision();
+ r3.rev = 2;
+
+ assertThat(r1)
+ .as("Revisions equality should should match expected value")
+ .isEqualTo(r1)
+ .isEqualTo(r2)
+ .isNotEqualTo(r3)
+ .isNotEqualTo(new Object());
+ }
+
+ @Test
+ void testHashCode() {
+ var r1 = new Revision();
+ r1.rev = 123;
+
+ var r2 = new Revision();
+ r2.rev = 123;
+
+ var r3 = new Revision();
+ r3.rev = 2;
+
+ assertThat(r1.hashCode()).isEqualTo(123).isEqualTo(r2.hashCode()).isNotEqualTo(r3.hashCode());
+ }
+
+ @Test
+ void testToString() {
+ var rev = new Revision();
+ rev.rev = 1;
+
+ assertThat(rev).as("Revision's toString should match expected value").hasToString("Revision{rev=1}");
+ }
+}
diff --git a/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/TestEntity.java b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/TestEntity.java
new file mode 100644
index 0000000..2dea9ea
--- /dev/null
+++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/TestEntity.java
@@ -0,0 +1,23 @@
+package ch.phoenix.oss.quarkus.commons.audit;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import org.hibernate.envers.Audited;
+
+@Entity
+@Audited
+@Table(name = "test_entity")
+public class TestEntity extends AuditedPanacheEntity {
+
+ public String something;
+
+ public TestEntity() {}
+
+ public TestEntity(String something) {
+ this.something = something;
+ }
+
+ public static TestEntity findBySomething(String something) {
+ return find("something", something).singleResult();
+ }
+}
diff --git a/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/TestEntity2.java b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/TestEntity2.java
new file mode 100644
index 0000000..922b96a
--- /dev/null
+++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/TestEntity2.java
@@ -0,0 +1,29 @@
+package ch.phoenix.oss.quarkus.commons.audit;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import org.hibernate.envers.Audited;
+
+@Entity
+@Audited
+@Table(name = "test_entity")
+public class TestEntity2 extends AuditedPanacheEntityBase {
+
+ @Id
+ @GeneratedValue
+ public Long id;
+
+ public String something;
+
+ public TestEntity2() {}
+
+ public TestEntity2(String something) {
+ this.something = something;
+ }
+
+ public static TestEntity2 findBySomething(String something) {
+ return find("something", something).singleResult();
+ }
+}
diff --git a/quarkus-audit-tools/src/test/resources/application.yaml b/quarkus-audit-tools/src/test/resources/application.yaml
new file mode 100644
index 0000000..d44d31a
--- /dev/null
+++ b/quarkus-audit-tools/src/test/resources/application.yaml
@@ -0,0 +1,22 @@
+quarkus:
+ flyway:
+ migrate-at-start: true
+ datasource:
+ db-kind: postgresql
+ hibernate-orm:
+ sql-load-script: no-file
+ schema-management:
+ strategy: none
+ log:
+ sql: true
+ bind-parameters: true
+ hibernate-envers:
+ audit-strategy: org.hibernate.envers.strategy.internal.ValidityAuditStrategy
+ revision-listener: ch.phoenix.oss.quarkus.commons.audit.AuditRevisionListener
+ security:
+ users:
+ embedded:
+ enabled: true
+ plain-text: true
+ users:
+ jon: doe
diff --git a/quarkus-audit-tools/src/test/resources/db/migration/V1__create_schema.sql b/quarkus-audit-tools/src/test/resources/db/migration/V1__create_schema.sql
new file mode 100644
index 0000000..daabe85
--- /dev/null
+++ b/quarkus-audit-tools/src/test/resources/db/migration/V1__create_schema.sql
@@ -0,0 +1,46 @@
+create sequence revinfo_seq start with 1 increment by 50;
+create sequence test_entity_seq start with 1 increment by 50;
+
+create table revinfo
+(
+ rev bigint not null,
+ timestamp timestamp(6) with time zone not null,
+ actor varchar(255),
+ spanId varchar(255),
+ traceId varchar(255),
+ requestId varchar(255),
+ clientIp varchar(255),
+ hostName varchar(255),
+ primary key (rev)
+);
+
+create table test_entity
+(
+
+ id bigint primary key not null,
+ something varchar(255),
+ createdAt timestamp(6) with time zone,
+ lastUpdatedAt timestamp(6) with time zone
+);
+
+create table test_entity_aud
+(
+ revtype smallint,
+ rev bigint not null,
+ revend bigint,
+ id bigint not null,
+ something varchar(255),
+ primary key (rev, id)
+);
+
+alter table if exists test_entity_aud
+ add constraint fk_rev__revinfo_rev
+ foreign key (rev)
+ references revinfo;
+
+
+alter table if exists test_entity_aud
+ add constraint fk_revend__revinfo_rev
+ foreign key (revend)
+ references revinfo;
+
diff --git a/quarkus-audit-tools/src/test/resources/db/migration/V2__create_audit_trigger.sql b/quarkus-audit-tools/src/test/resources/db/migration/V2__create_audit_trigger.sql
new file mode 100644
index 0000000..f76a2cb
--- /dev/null
+++ b/quarkus-audit-tools/src/test/resources/db/migration/V2__create_audit_trigger.sql
@@ -0,0 +1,35 @@
+CREATE OR REPLACE FUNCTION trg_test_entity_aud_apply_rev()
+ RETURNS TRIGGER
+ LANGUAGE plpgsql
+AS $func$
+DECLARE
+ ts TIMESTAMP;
+BEGIN
+ -- fetch the exact revision timestamp from revinfo
+ SELECT r.timestamp
+ INTO ts
+ FROM revinfo r
+ WHERE r.rev = NEW.rev;
+
+ -- only set created_at once, when still NULL
+ UPDATE test_entity
+ SET createdAt = ts
+ WHERE id = NEW.id
+ AND createdAt IS NULL;
+
+ -- always bump last_updated_at
+ UPDATE test_entity
+ SET lastUpdatedAt = ts
+ WHERE id = NEW.id;
+
+ RETURN NULL;
+END;
+$func$;
+
+DROP TRIGGER IF EXISTS trg_test_entity_aud_after_insert
+ ON test_entity_aud;
+
+CREATE TRIGGER trg_test_entity_aud_after_insert
+ AFTER INSERT ON test_entity_aud
+ FOR EACH ROW
+EXECUTE FUNCTION trg_test_entity_aud_apply_rev();
diff --git a/quarkus-audit-tools/src/test/resources/import.sql b/quarkus-audit-tools/src/test/resources/import.sql
new file mode 100644
index 0000000..c4a0b85
--- /dev/null
+++ b/quarkus-audit-tools/src/test/resources/import.sql
@@ -0,0 +1,37 @@
+-- 1) Create or replace the trigger function
+CREATE OR REPLACE FUNCTION trg_test_entity_aud_apply_rev()
+ RETURNS TRIGGER
+ LANGUAGE plpgsql
+AS $func$
+DECLARE
+ ts TIMESTAMP;
+BEGIN
+ -- fetch the exact revision timestamp from revinfo
+ SELECT r.timestamp
+ INTO ts
+ FROM revinfo r
+ WHERE r.rev = NEW.rev;
+
+ -- only set created_at once, when still NULL
+ UPDATE test_entity
+ SET created_at = ts
+ WHERE id = NEW.id
+ AND created_at IS NULL;
+
+ -- always bump last_updated_at
+ UPDATE test_entity
+ SET last_updated_at = ts
+ WHERE id = NEW.id;
+
+ RETURN NULL; -- AFTER trigger ignores return value
+END;
+$func$;
+
+-- 2) Drop any existing trigger, then attach the new one
+DROP TRIGGER IF EXISTS trg_test_entity_aud_after_insert
+ ON test_entity_aud;
+
+CREATE TRIGGER trg_test_entity_aud_after_insert
+ AFTER INSERT ON test_entity_aud
+ FOR EACH ROW
+EXECUTE FUNCTION trg_test_entity_aud_apply_rev();
diff --git a/quarkus-client-logger/pom.xml b/quarkus-client-logger/pom.xml
new file mode 100644
index 0000000..3095369
--- /dev/null
+++ b/quarkus-client-logger/pom.xml
@@ -0,0 +1,65 @@
+
+
+ 4.0.0
+
+
+ ch.phoenix.oss
+ quarkus-commons
+ 1.0.9-SNAPSHOT
+
+
+ quarkus-client-logger
+ jar
+
+
+
+ io.quarkus
+ quarkus-rest-client
+
+
+ io.quarkus
+ quarkus-config-yaml
+ test
+
+
+ io.quarkus
+ quarkus-rest-jackson
+ test
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco-plugin.version}
+
+
+ jacoco-check
+
+ check
+
+ test
+
+ ${project.build.directory}/jacoco-quarkus.exec
+
+
+ BUNDLE
+
+
+ INSTRUCTION
+ COVEREDRATIO
+ 0.92
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/LowerCaseStringConverter.java b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/LowerCaseStringConverter.java
new file mode 100644
index 0000000..498e318
--- /dev/null
+++ b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/LowerCaseStringConverter.java
@@ -0,0 +1,10 @@
+package ch.phoenix.oss.quarkus.commons.client.logger;
+
+import org.eclipse.microprofile.config.spi.Converter;
+
+public class LowerCaseStringConverter implements Converter {
+ @Override
+ public String convert(String value) throws IllegalArgumentException, NullPointerException {
+ return value.toLowerCase();
+ }
+}
diff --git a/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLogger.java b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLogger.java
new file mode 100644
index 0000000..cb2b63b
--- /dev/null
+++ b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLogger.java
@@ -0,0 +1,119 @@
+package ch.phoenix.oss.quarkus.commons.client.logger;
+
+import io.vertx.core.Handler;
+import io.vertx.core.MultiMap;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.http.HttpClientResponse;
+import jakarta.enterprise.context.Dependent;
+import jakarta.inject.Inject;
+import java.util.Map;
+import java.util.Set;
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.reactive.client.api.ClientLogger;
+
+/**
+ * This is based on org.jboss.resteasy.reactive.client.logging.DefaultClientLogger,
+ * with the only change being that headers are redacted based on the Set provided
+ * by the configuration.
+ */
+@Dependent
+public class RedactingClientLogger implements ClientLogger {
+
+ private static final Logger log = Logger.getLogger(RedactingClientLogger.class);
+
+ private static final String REDACTED_VALUE = "*****";
+
+ private final Set redactedHeaders;
+
+ private int bodySize;
+
+ @Inject
+ public RedactingClientLogger(RedactingClientLoggerConfiguration configuration) {
+ this.redactedHeaders = configuration
+ .headers()
+ .redact()
+ .orElse(RedactingClientLoggerConfiguration.Headers.DEFAULT_REDACTED_HEADERS);
+ }
+
+ @Override
+ public void setBodySize(int bodySize) {
+ this.bodySize = bodySize;
+ }
+
+ @Override
+ public void logResponse(HttpClientResponse response, boolean redirect) {
+ if (!log.isDebugEnabled()) {
+ return;
+ }
+
+ //noinspection Convert2Lambda
+ response.bodyHandler(new Handler<>() {
+ @Override
+ public void handle(Buffer body) {
+ log.debugf(
+ "%s: %s %s, Status[%d %s], Headers[%s], Body:\n%s",
+ redirect ? "Redirect" : "Response",
+ response.request().getMethod(),
+ response.request().absoluteURI(),
+ response.statusCode(),
+ response.statusMessage(),
+ asString(response.headers()),
+ bodyToString(body));
+ }
+ });
+ }
+
+ @Override
+ public void logRequest(HttpClientRequest request, Buffer body, boolean omitBody) {
+ if (!log.isDebugEnabled()) {
+ return;
+ }
+ if (omitBody) {
+ log.debugf(
+ "Request: %s %s Headers[%s], Body omitted",
+ request.getMethod(), request.absoluteURI(), asString(request.headers()));
+ } else if (body == null || body.length() == 0) {
+ log.debugf(
+ "Request: %s %s Headers[%s], Empty body",
+ request.getMethod(), request.absoluteURI(), asString(request.headers()));
+ } else {
+ log.debugf(
+ "Request: %s %s Headers[%s], Body:\n%s",
+ request.getMethod(), request.absoluteURI(), asString(request.headers()), bodyToString(body));
+ }
+ }
+
+ private String bodyToString(Buffer body) {
+ if (body == null) {
+ return "";
+ } else if (bodySize <= 0) {
+ return body.toString();
+ } else {
+ String bodyAsString = body.toString();
+ return bodyAsString.substring(0, Math.min(bodySize, bodyAsString.length()));
+ }
+ }
+
+ private String asString(MultiMap headers) {
+ if (headers.isEmpty()) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder((headers.size() * (6 + 1 + 6))
+ + (headers.size() - 1)); // this is a very rough estimate of a result like 'key1=value1 key2=value2'
+ boolean isFirst = true;
+ for (Map.Entry entry : headers) {
+ if (isFirst) {
+ isFirst = false;
+ } else {
+ sb.append(' ');
+ }
+
+ var key = entry.getKey();
+ var value = redactedHeaders.contains(key.toLowerCase()) ? REDACTED_VALUE : entry.getValue();
+
+ sb.append(key).append('=').append(value);
+ }
+ return sb.toString();
+ }
+}
diff --git a/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLoggerConfiguration.java b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLoggerConfiguration.java
new file mode 100644
index 0000000..24fa05f
--- /dev/null
+++ b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLoggerConfiguration.java
@@ -0,0 +1,20 @@
+package ch.phoenix.oss.quarkus.commons.client.logger;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithConverter;
+import jakarta.ws.rs.core.HttpHeaders;
+import java.util.Optional;
+import java.util.Set;
+
+@ConfigMapping(prefix = "phoenix.client-logger")
+public interface RedactingClientLoggerConfiguration {
+
+ Headers headers();
+
+ interface Headers {
+
+ Set DEFAULT_REDACTED_HEADERS = Set.of(HttpHeaders.AUTHORIZATION.toLowerCase());
+
+ Optional> redact();
+ }
+}
diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/InfoLevelProfile.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/InfoLevelProfile.java
new file mode 100644
index 0000000..35acb14
--- /dev/null
+++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/InfoLevelProfile.java
@@ -0,0 +1,12 @@
+package ch.phoenix.oss.quarkus.commons.client.logger;
+
+import io.quarkus.test.junit.QuarkusTestProfile;
+import java.util.Map;
+
+public class InfoLevelProfile implements QuarkusTestProfile {
+
+ @Override
+ public Map getConfigOverrides() {
+ return Map.of("quarkus.log.category.\"ch.phoenix.oss.quarkus.commons.client.logger\".level", "INFO");
+ }
+}
diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/InfoLevelTest.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/InfoLevelTest.java
new file mode 100644
index 0000000..c6b27ef
--- /dev/null
+++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/InfoLevelTest.java
@@ -0,0 +1,44 @@
+package ch.phoenix.oss.quarkus.commons.client.logger;
+
+import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.TestProfile;
+import jakarta.inject.Inject;
+import java.net.URI;
+import java.util.Optional;
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+@TestProfile(InfoLevelProfile.class)
+class InfoLevelTest {
+
+ @Inject
+ @RestClient
+ TestClient injectedClient;
+
+ TestClient builtClient = QuarkusRestClientBuilder.newBuilder()
+ .clientLogger(new RedactingClientLogger(() -> Optional::empty))
+ .baseUri(URI.create("http://localhost:8087"))
+ .build(TestClient.class);
+
+ @Test
+ void getWithInjectedClient() {
+ injectedClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted");
+ }
+
+ @Test
+ void getWithBuiltClientAndEmptyConfig() {
+ builtClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted");
+ }
+
+ @Test
+ void postWithInjectedClient() {
+ injectedClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted", "body");
+ }
+
+ @Test
+ void postWithBuiltClientAndEmptyConfig() {
+ builtClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted", "");
+ }
+}
diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLoggerTest.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLoggerTest.java
new file mode 100644
index 0000000..01e9389
--- /dev/null
+++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLoggerTest.java
@@ -0,0 +1,47 @@
+package ch.phoenix.oss.quarkus.commons.client.logger;
+
+import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import java.net.URI;
+import java.util.Optional;
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class RedactingClientLoggerTest {
+
+ @Inject
+ @RestClient
+ TestClient injectedClient;
+
+ TestClient builtClient = QuarkusRestClientBuilder.newBuilder()
+ .clientLogger(new RedactingClientLogger(() -> Optional::empty))
+ .baseUri(URI.create("http://localhost:8087"))
+ .build(TestClient.class);
+
+ @Test
+ void getWithInjectedClient() {
+ injectedClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted");
+ }
+
+ @Test
+ void getWithBuiltClientAndEmptyConfig() {
+ builtClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted");
+ }
+
+ @Test
+ void postWithInjectedClient() {
+ injectedClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted", "body");
+ }
+
+ @Test
+ void postWithInjectedClientAndNullBody() {
+ injectedClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted", null);
+ }
+
+ @Test
+ void postWithBuiltClientAndEmptyConfig() {
+ builtClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted", "");
+ }
+}
diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/ScopeNoneProfile.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/ScopeNoneProfile.java
new file mode 100644
index 0000000..42daa28
--- /dev/null
+++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/ScopeNoneProfile.java
@@ -0,0 +1,12 @@
+package ch.phoenix.oss.quarkus.commons.client.logger;
+
+import io.quarkus.test.junit.QuarkusTestProfile;
+import java.util.Map;
+
+public class ScopeNoneProfile implements QuarkusTestProfile {
+
+ @Override
+ public Map getConfigOverrides() {
+ return Map.of("quarkus.rest-client.logging.scope", "none");
+ }
+}
diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/ScopeNoneTest.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/ScopeNoneTest.java
new file mode 100644
index 0000000..14538b2
--- /dev/null
+++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/ScopeNoneTest.java
@@ -0,0 +1,44 @@
+package ch.phoenix.oss.quarkus.commons.client.logger;
+
+import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.TestProfile;
+import jakarta.inject.Inject;
+import java.net.URI;
+import java.util.Optional;
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+@TestProfile(ScopeNoneProfile.class)
+class ScopeNoneTest {
+
+ @Inject
+ @RestClient
+ TestClient injectedClient;
+
+ TestClient builtClient = QuarkusRestClientBuilder.newBuilder()
+ .clientLogger(new RedactingClientLogger(() -> Optional::empty))
+ .baseUri(URI.create("http://localhost:8087"))
+ .build(TestClient.class);
+
+ @Test
+ void getWithInjectedClient() {
+ injectedClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted");
+ }
+
+ @Test
+ void getWithBuiltClientAndEmptyConfig() {
+ builtClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted");
+ }
+
+ @Test
+ void postWithInjectedClient() {
+ injectedClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted", "body");
+ }
+
+ @Test
+ void postWithBuiltClientAndEmptyConfig() {
+ builtClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted", "");
+ }
+}
diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/TestClient.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/TestClient.java
new file mode 100644
index 0000000..79769ac
--- /dev/null
+++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/TestClient.java
@@ -0,0 +1,28 @@
+package ch.phoenix.oss.quarkus.commons.client.logger;
+
+import jakarta.ws.rs.*;
+import jakarta.ws.rs.core.MediaType;
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+
+@SuppressWarnings("UastIncorrectHttpHeaderInspection")
+@RegisterRestClient(configKey = "test")
+public interface TestClient {
+
+ @GET
+ @Path("/")
+ @Produces(MediaType.TEXT_PLAIN)
+ String get(
+ @HeaderParam("Authorization") String authorization,
+ @HeaderParam("X-Request-ID") String requestId,
+ @HeaderParam("X-Something-Else") String somethingElse);
+
+ @POST
+ @Path("/")
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ String post(
+ @HeaderParam("Authorization") String authorization,
+ @HeaderParam("X-Request-ID") String requestId,
+ @HeaderParam("X-Something-Else") String somethingElse,
+ String body);
+}
diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/TestResource.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/TestResource.java
new file mode 100644
index 0000000..c83bdd0
--- /dev/null
+++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/TestResource.java
@@ -0,0 +1,21 @@
+package ch.phoenix.oss.quarkus.commons.client.logger;
+
+import jakarta.ws.rs.*;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/")
+public class TestResource {
+
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ public String get() {
+ return "get";
+ }
+
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String post(String body) {
+ return body;
+ }
+}
diff --git a/quarkus-client-logger/src/test/resources/application.yaml b/quarkus-client-logger/src/test/resources/application.yaml
new file mode 100644
index 0000000..a02f41d
--- /dev/null
+++ b/quarkus-client-logger/src/test/resources/application.yaml
@@ -0,0 +1,20 @@
+quarkus:
+ http:
+ test-port: 8087
+ rest-client:
+ logging:
+ scope: request-response
+ body-limit: 10000
+ test:
+ url: http://localhost:${quarkus.http.test-port}
+ log:
+ category:
+ "ch.phoenix.oss.quarkus.commons.client.logger":
+ level: DEBUG
+
+phoenix:
+ client-logger:
+ headers:
+ redact:
+ - AUTHORIZATION
+ - X-SOMETHING-ELSE
diff --git a/quarkus-clock-service/pom.xml b/quarkus-clock-service/pom.xml
index f918d10..aaab0c1 100644
--- a/quarkus-clock-service/pom.xml
+++ b/quarkus-clock-service/pom.xml
@@ -5,7 +5,7 @@
ch.phoenix.oss
quarkus-commons
- 1.0.0
+ 1.0.9-SNAPSHOT
quarkus-clock-service
diff --git a/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockService.java b/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockService.java
index e9a214e..f6c3c26 100644
--- a/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockService.java
+++ b/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockService.java
@@ -1,10 +1,19 @@
package ch.phoenix.oss.quarkus.commons.clock;
import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
public interface ClockService {
Instant instant();
long currentTimeMillis();
+
+ LocalDate localDate();
+
+ LocalDateTime localDateTime();
+
+ ZonedDateTime zonedDateTime();
}
diff --git a/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImpl.java b/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImpl.java
index 4c90334..e47c757 100644
--- a/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImpl.java
+++ b/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImpl.java
@@ -2,8 +2,7 @@ package ch.phoenix.oss.quarkus.commons.clock;
import io.quarkus.arc.DefaultBean;
import jakarta.enterprise.context.ApplicationScoped;
-import java.time.Clock;
-import java.time.Instant;
+import java.time.*;
@DefaultBean
@ApplicationScoped
@@ -24,4 +23,19 @@ class ClockServiceImpl implements ClockService {
public long currentTimeMillis() {
return clock.millis();
}
+
+ @Override
+ public LocalDate localDate() {
+ return LocalDate.now(clock);
+ }
+
+ @Override
+ public LocalDateTime localDateTime() {
+ return LocalDateTime.now(clock);
+ }
+
+ @Override
+ public ZonedDateTime zonedDateTime() {
+ return ZonedDateTime.now(clock);
+ }
}
diff --git a/quarkus-clock-service/src/test/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImplTest.java b/quarkus-clock-service/src/test/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImplTest.java
index 626bb0a..dfffea8 100644
--- a/quarkus-clock-service/src/test/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImplTest.java
+++ b/quarkus-clock-service/src/test/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImplTest.java
@@ -6,8 +6,7 @@ import static org.mockito.Mockito.when;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
-import java.time.Clock;
-import java.time.Instant;
+import java.time.*;
import org.junit.jupiter.api.Test;
@QuarkusTest
@@ -38,4 +37,34 @@ class ClockServiceImplTest {
.as("Instant should match expected value")
.isEqualTo(expected);
}
+
+ @Test
+ void localDate() {
+ var instant = Instant.ofEpochMilli(1729280640915L);
+ when(clock.instant()).thenReturn(instant);
+ when(clock.getZone()).thenReturn(ZoneId.of("UTC"));
+ assertThat(clockService.localDate())
+ .as("LocalDate should match expected value")
+ .isEqualTo(LocalDate.parse("2024-10-18"));
+ }
+
+ @Test
+ void localDateTime() {
+ var instant = Instant.ofEpochMilli(1729280640915L);
+ when(clock.instant()).thenReturn(instant);
+ when(clock.getZone()).thenReturn(ZoneId.of("UTC"));
+ assertThat(clockService.localDateTime())
+ .as("LocalDateTime should match expected value")
+ .isEqualTo(LocalDateTime.parse("2024-10-18T19:44:00.915"));
+ }
+
+ @Test
+ void zonedDateTime() {
+ var instant = Instant.ofEpochMilli(1729280640915L);
+ when(clock.instant()).thenReturn(instant);
+ when(clock.getZone()).thenReturn(ZoneId.of("UTC"));
+ assertThat(clockService.zonedDateTime())
+ .as("ZonedDateTime should match expected value")
+ .isEqualTo(ZonedDateTime.parse("2024-10-18T19:44:00.915Z[UTC]"));
+ }
}
diff --git a/quarkus-json-service/pom.xml b/quarkus-json-service/pom.xml
index 75bfe33..717c39f 100644
--- a/quarkus-json-service/pom.xml
+++ b/quarkus-json-service/pom.xml
@@ -5,7 +5,7 @@
ch.phoenix.oss
quarkus-commons
- 1.0.0
+ 1.0.9-SNAPSHOT
quarkus-json-service
diff --git a/quarkus-message-digest-service/pom.xml b/quarkus-message-digest-service/pom.xml
index 48389f8..c5d2c2d 100644
--- a/quarkus-message-digest-service/pom.xml
+++ b/quarkus-message-digest-service/pom.xml
@@ -5,7 +5,7 @@
ch.phoenix.oss
quarkus-commons
- 1.0.0
+ 1.0.9-SNAPSHOT
quarkus-message-digest-service
diff --git a/quarkus-random-number-generator/pom.xml b/quarkus-random-number-generator/pom.xml
index bc4dd84..940f965 100644
--- a/quarkus-random-number-generator/pom.xml
+++ b/quarkus-random-number-generator/pom.xml
@@ -5,7 +5,7 @@
ch.phoenix.oss
quarkus-commons
- 1.0.0
+ 1.0.9-SNAPSHOT
quarkus-random-number-generator
diff --git a/quarkus-tracing-service/pom.xml b/quarkus-tracing-service/pom.xml
new file mode 100644
index 0000000..214e936
--- /dev/null
+++ b/quarkus-tracing-service/pom.xml
@@ -0,0 +1,78 @@
+
+
+ 4.0.0
+
+
+ ch.phoenix.oss
+ quarkus-commons
+ 1.0.9-SNAPSHOT
+
+
+ quarkus-tracing-service
+ jar
+
+
+
+ io.quarkus
+ quarkus-rest-jackson
+
+
+ io.quarkus
+ quarkus-opentelemetry
+
+
+ io.quarkus
+ quarkus-security
+
+
+ io.quarkus
+ quarkus-elytron-security-properties-file
+ test
+
+
+ io.quarkus
+ quarkus-config-yaml
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco-plugin.version}
+
+
+ jacoco-check
+
+ check
+
+ test
+
+ ${project.build.directory}/jacoco-quarkus.exec
+
+
+ BUNDLE
+
+
+ INSTRUCTION
+ COVEREDRATIO
+ 0.95
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/LowerCaseStringConverter.java b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/LowerCaseStringConverter.java
new file mode 100644
index 0000000..ede1518
--- /dev/null
+++ b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/LowerCaseStringConverter.java
@@ -0,0 +1,10 @@
+package ch.phoenix.oss.quarkus.commons.tracing;
+
+import org.eclipse.microprofile.config.spi.Converter;
+
+public class LowerCaseStringConverter implements Converter {
+ @Override
+ public String convert(String value) throws IllegalArgumentException, NullPointerException {
+ return value.toLowerCase();
+ }
+}
diff --git a/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingConfiguration.java b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingConfiguration.java
new file mode 100644
index 0000000..1093348
--- /dev/null
+++ b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingConfiguration.java
@@ -0,0 +1,58 @@
+package ch.phoenix.oss.quarkus.commons.tracing;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithConverter;
+import io.smallrye.config.WithDefault;
+import jakarta.ws.rs.core.HttpHeaders;
+import java.util.Optional;
+import java.util.Set;
+
+@ConfigMapping(prefix = "phoenix.commons.tracing")
+public interface TracingConfiguration {
+
+ RequestFilterConfiguration requestFilter();
+
+ interface RequestFilterConfiguration {
+
+ Headers headers();
+
+ interface Headers {
+
+ Set DEFAULT_REDACTED = Set.of(HttpHeaders.AUTHORIZATION.toLowerCase());
+
+ /**
+ * Optional set of headers to redact when tracing. By default, redacts
+ * the 'Authorization' header.
+ *
+ * @return the set of headers to be redacted
+ */
+ Optional> redact();
+ }
+
+ Path path();
+
+ interface Path {
+
+ @WithDefault("false")
+ boolean includeRaw();
+ }
+
+ Query query();
+
+ interface Query {
+
+ Set DEFAULT_REDACTED = Set.of("access_token", "refresh_token", "apikey");
+
+ @WithDefault("false")
+ boolean includeRaw();
+
+ /**
+ * Optional set of query params to redact when tracing. By default, redacts
+ * the following params: 'access_token', 'refresh_token' and 'apikey'.
+ *
+ * @return the set of query params to be redacted
+ */
+ Optional> redact();
+ }
+ }
+}
diff --git a/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingConstants.java b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingConstants.java
new file mode 100644
index 0000000..7022a22
--- /dev/null
+++ b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingConstants.java
@@ -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() {}
+}
diff --git a/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingRequestFilter.java b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingRequestFilter.java
new file mode 100644
index 0000000..7bb2427
--- /dev/null
+++ b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingRequestFilter.java
@@ -0,0 +1,151 @@
+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 org.jboss.resteasy.reactive.server.ServerRequestFilter;
+
+@Unremovable
+public class TracingRequestFilter {
+
+ private static final String REDACTED_VALUE = "********";
+
+ 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());
+ }
+
+ var redactedHeaders = configuration
+ .requestFilter()
+ .headers()
+ .redact()
+ .orElse(TracingConfiguration.RequestFilterConfiguration.Headers.DEFAULT_REDACTED);
+
+ requestContext.getHeaders().forEach((key, value) -> {
+ var lowerCaseKey = key.toLowerCase();
+ var property = TracingConstants.REQUEST_HEADERS + '.' + lowerCaseKey;
+ if (redactedHeaders.contains(lowerCaseKey)) {
+ tracingService.trace(property, REDACTED_VALUE);
+ } else {
+ tracingService.trace(property, joinStrings(value));
+ }
+ });
+
+ var redactedQueryParams = configuration
+ .requestFilter()
+ .query()
+ .redact()
+ .orElse(TracingConfiguration.RequestFilterConfiguration.Query.DEFAULT_REDACTED);
+
+ uriInfo.getQueryParameters().forEach((key, value) -> {
+ var lowerCaseKey = key.toLowerCase();
+ var property = TracingConstants.REQUEST_QUERY_PARAMS + '.' + lowerCaseKey;
+
+ if (redactedQueryParams.contains(lowerCaseKey)) {
+ tracingService.trace(property, REDACTED_VALUE);
+ } else {
+ tracingService.trace(property, 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());
+
+ Log.debugf("Incoming request: %s %s", method, uriInfo.getAbsolutePath().getRawPath());
+ }
+
+ private static String joinStrings(List 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);
+ }
+}
diff --git a/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingService.java b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingService.java
new file mode 100644
index 0000000..812867d
--- /dev/null
+++ b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingService.java
@@ -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 getRequestPathRaw();
+
+ String getRequestMethod();
+
+ String getRequestId();
+
+ String getTraceId();
+
+ String getSpanId();
+
+ String getClientIp();
+
+ String getSchedulerJob();
+}
diff --git a/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingServiceImpl.java b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingServiceImpl.java
new file mode 100644
index 0000000..d46f684
--- /dev/null
+++ b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingServiceImpl.java
@@ -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.tracef("tracing key=%s value=%s", key, value);
+ MDC.put(key, value);
+ }
+
+ @Override
+ public String getActor() {
+ return (String) MDC.get(TracingConstants.ACTOR);
+ }
+
+ @Override
+ public String getRequestPathRaw() {
+ 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);
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/ActorTest.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/ActorTest.java
new file mode 100644
index 0000000..8ab52eb
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/ActorTest.java
@@ -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
+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", "********");
+ 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);
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/QueryParamTest.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/QueryParamTest.java
new file mode 100644
index 0000000..10b3894
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/QueryParamTest.java
@@ -0,0 +1,54 @@
+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
+class QueryParamTest {
+
+ @InjectSpy
+ TracingService tracingService;
+
+ @Test
+ void traceQueryParams() {
+ var route = "/authenticated";
+ RestAssured.given()
+ .auth()
+ .basic("jon", "doe")
+ .accept(ContentType.TEXT)
+ .header("X-SOMETHING-ELSE", "whatever")
+ .queryParam("access_token", "api123")
+ .queryParam("refresh_token", "refresh123")
+ .queryParam("apikey", "apikey123")
+ .queryParam("grant_type", "authorization_code")
+ .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", "********");
+ 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.headers.x-something-else", "whatever");
+ verify(tracingService).trace("request.query.params.access_token", "********");
+ verify(tracingService).trace("request.query.params.refresh_token", "********");
+ verify(tracingService).trace("request.query.params.apikey", "********");
+ verify(tracingService).trace("request.query.params.grant_type", "authorization_code");
+ verify(tracingService).trace("request.client.ip", "127.0.0.1");
+ verifyNoMoreInteractions(tracingService);
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RawPathTest.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RawPathTest.java
new file mode 100644
index 0000000..e3579ba
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RawPathTest.java
@@ -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)
+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);
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RedactedTest.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RedactedTest.java
new file mode 100644
index 0000000..c928917
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RedactedTest.java
@@ -0,0 +1,61 @@
+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)
+class RedactedTest {
+
+ @InjectSpy
+ TracingService tracingService;
+
+ @Test
+ void traceRedactedValues() {
+ var route = "/authenticated";
+ RestAssured.given()
+ .auth()
+ .basic("jon", "doe")
+ .accept(ContentType.TEXT)
+ .header("X-SOMETHING-ELSE", "whatever")
+ .queryParam("access_token", "api123")
+ .queryParam("refresh_token", "refresh123")
+ .queryParam("apikey", "apikey123")
+ .queryParam("grant_type", "authorization_code")
+ .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.path.raw", route);
+ verify(tracingService).trace("request.headers.accept", "text/plain");
+ verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate");
+ verify(tracingService).trace("request.headers.authorization", "********");
+ 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.headers.x-something-else", "********");
+ verify(tracingService).trace("request.query.params.access_token", "********");
+ verify(tracingService).trace("request.query.params.refresh_token", "refresh123");
+ verify(tracingService).trace("request.query.params.apikey", "apikey123");
+ verify(tracingService).trace("request.query.params.grant_type", "authorization_code");
+ verify(tracingService)
+ .trace(
+ "request.query.raw",
+ "access_token=api123&refresh_token=refresh123&apikey=apikey123&grant_type=authorization_code");
+ verify(tracingService).trace("request.client.ip", "127.0.0.1");
+ verifyNoMoreInteractions(tracingService);
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RoutePatternTest.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RoutePatternTest.java
new file mode 100644
index 0000000..156c0a4
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RoutePatternTest.java
@@ -0,0 +1,87 @@
+package ch.phoenix.oss.quarkus.commons.tracing;
+
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+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 java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+@QuarkusTest
+class RoutePatternTest {
+
+ @InjectSpy
+ TracingService tracingService;
+
+ static Stream get() {
+ return Stream.of(
+ arguments("/", Map.of()),
+ arguments("/leading-and-no-trailing", Map.of()),
+ arguments("/leading/{param}/{param2}", Map.of("param", "1", "param2", "2")),
+ arguments("/{param}/{param2}/trailing", Map.of("param", "1", "param2", "2")),
+ arguments("/leading-and-no-trailing/{param}", Map.of("param", "1")),
+ arguments("/leading-and-no-trailing/{param}/{param2}", Map.of("param", "1", "param2", "2")),
+ arguments("/leading-and-trailing", Map.of()),
+ arguments("/leading-and-trailing/{param}", Map.of("param", "1")),
+ arguments("/leading-and-trailing/{param}/{param2}", Map.of("param", "1", "param2", "2")),
+ arguments("/no-leading-and-no-trailing", Map.of()),
+ arguments("/no-leading-and-no-trailing/{param}", Map.of("param", "1")),
+ arguments("/no-leading-and-no-trailing/{param}/{param2}", Map.of("param", "1", "param2", "2")),
+ arguments("/no-leading-and-trailing", Map.of()),
+ arguments("/no-leading-and-trailing/{param}", Map.of("param", "1")),
+ arguments("/no-leading-and-trailing/{param}/{param2}", Map.of("param", "1", "param2", "2")));
+ }
+
+ @MethodSource
+ @ParameterizedTest
+ void get(String route, Map pathParams) {
+ RestAssured.given()
+ .accept(ContentType.TEXT)
+ .when()
+ .get(route, pathParams)
+ .then()
+ .statusCode(200);
+
+ 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);
+ }
+
+ @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);
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/Test2Profile.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/Test2Profile.java
new file mode 100644
index 0000000..f98dcff
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/Test2Profile.java
@@ -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";
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/TracingServiceImplTest.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/TracingServiceImplTest.java
new file mode 100644
index 0000000..f728e14
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/TracingServiceImplTest.java
@@ -0,0 +1,98 @@
+package ch.phoenix.oss.quarkus.commons.tracing;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.instrumentation.annotations.WithSpan;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import org.jboss.logging.MDC;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class TracingServiceImplTest {
+
+ @Inject
+ TracingService tracingService;
+
+ @Inject
+ Span span;
+
+ @BeforeEach
+ void setUp() {
+ MDC.clear();
+ }
+
+ @Test
+ void getActor() {
+ tracingService.trace("actor", "abc");
+ assertThat(tracingService.getActor())
+ .as("Actor should match expected value")
+ .isEqualTo("abc");
+ }
+
+ @Test
+ void getRequestPathRaw() {
+ tracingService.trace("request.path.raw", "/foo/bar");
+ assertThat(tracingService.getRequestPathRaw())
+ .as("Request Path Raw should match expected value")
+ .isEqualTo("/foo/bar");
+ }
+
+ @Test
+ void getRequestMethod() {
+ tracingService.trace("request.method", "GET");
+ assertThat(tracingService.getRequestMethod())
+ .as("Request Method should match expected value")
+ .isEqualTo("GET");
+ }
+
+ @Test
+ void getRequestId() {
+ tracingService.trace("request.headers.x-request-id", "ba458367-bfeb-46ba-87da-50b9343be8f9");
+ assertThat(tracingService.getRequestId())
+ .as("Request Id should match expected value")
+ .isEqualTo("ba458367-bfeb-46ba-87da-50b9343be8f9");
+ }
+
+ @Test
+ @WithSpan
+ void getTraceId() {
+ assertThat(tracingService.getTraceId())
+ .as("Request Trace Id should match expected value")
+ .isEqualTo(span.getSpanContext().getTraceId());
+ }
+
+ @Test
+ @WithSpan
+ void getSpanId() {
+ assertThat(tracingService.getSpanId())
+ .as("Request Span Id should match expected value")
+ .isEqualTo(span.getSpanContext().getSpanId());
+ }
+
+ @Test
+ void getClientIp() {
+ tracingService.trace("request.client.ip", "127.0.0.1");
+ assertThat(tracingService.getClientIp())
+ .as("Request Client Iü should match expected value")
+ .isEqualTo("127.0.0.1");
+ }
+
+ @Test
+ void getSchedulerJob() {
+ tracingService.trace("scheduler.job.name", "scheduler/abc");
+ assertThat(tracingService.getSchedulerJob())
+ .as("Scheduler Job Name should match expected value")
+ .isEqualTo("scheduler/abc");
+ }
+
+ @Test
+ void clearAll() {
+ tracingService.trace("aaa", "bbb");
+ assertThat(MDC.get("aaa")).isEqualTo("bbb");
+ tracingService.clearAll();
+ assertThat(MDC.get("aaa")).isNull();
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/AuthenticatedResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/AuthenticatedResource.java
new file mode 100644
index 0000000..d18b967
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/AuthenticatedResource.java
@@ -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";
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/BlankResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/BlankResource.java
new file mode 100644
index 0000000..04c5df9
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/BlankResource.java
@@ -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";
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/LeadingAndNoTrailingResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/LeadingAndNoTrailingResource.java
new file mode 100644
index 0000000..b03a7ae
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/LeadingAndNoTrailingResource.java
@@ -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;
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/LeadingAndTrailingResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/LeadingAndTrailingResource.java
new file mode 100644
index 0000000..1dbc175
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/LeadingAndTrailingResource.java
@@ -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;
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/NoLeadingAndNoTrailingResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/NoLeadingAndNoTrailingResource.java
new file mode 100644
index 0000000..d75dade
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/NoLeadingAndNoTrailingResource.java
@@ -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;
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/NoLeadingAndTrailingResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/NoLeadingAndTrailingResource.java
new file mode 100644
index 0000000..9c9805a
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/NoLeadingAndTrailingResource.java
@@ -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;
+ }
+}
diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/SlashResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/SlashResource.java
new file mode 100644
index 0000000..8c8bdc7
--- /dev/null
+++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/SlashResource.java
@@ -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/{param}/{param2}")
+ public String doubleLeading(int param, int param2) {
+ return "leading/" + param + "/" + param2;
+ }
+
+ @GET
+ @Path("{param}/{param2}/trailing/")
+ public String doubleTrailing(int param, int param2) {
+ return param + "/" + param2 + "/trailing";
+ }
+}
diff --git a/quarkus-tracing-service/src/test/resources/application.yaml b/quarkus-tracing-service/src/test/resources/application.yaml
new file mode 100644
index 0000000..3c6bb6f
--- /dev/null
+++ b/quarkus-tracing-service/src/test/resources/application.yaml
@@ -0,0 +1,39 @@
+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
+ - X-SOMETHING-ELSE
+ query:
+ include-raw: true
+ redact:
+ - ACCESS_TOKEN
\ No newline at end of file
diff --git a/quarkus-uuid-generator/pom.xml b/quarkus-uuid-generator/pom.xml
index b81e68d..a6c395c 100644
--- a/quarkus-uuid-generator/pom.xml
+++ b/quarkus-uuid-generator/pom.xml
@@ -5,7 +5,7 @@
ch.phoenix.oss
quarkus-commons
- 1.0.0
+ 1.0.9-SNAPSHOT
quarkus-uuid-generator