diff --git a/.github/project.yaml b/.github/project.yaml index 40e3006..0b40189 100644 --- a/.github/project.yaml +++ b/.github/project.yaml @@ -1,4 +1,5 @@ name: Quarkus Commons release: - current-version: "1.0.0" - next-version: "1.0.1-SNAPSHOT" + current-version: "1.0.8" + next-version: "1.0.9-SNAPSHOT" + diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b32c3db..1272b20 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,6 +4,8 @@ on: push: branches: - "main" + tags: + - '[0-9]+.[0-9]+.[0-9]+' paths-ignore: - '.gitattributes' - '.gitignore' @@ -12,6 +14,7 @@ on: - 'docs/**' - 'README.md' pull_request: + workflow_dispatch: env: COMMON_MAVEN_OPTS: "-e -B --fae" @@ -60,19 +63,24 @@ jobs: servers: | [{ "id": "phoenix-oss", - "configuration": { - "httpHeaders": { - "property": { - "name": "Authorization", - "value": "token ${{ secrets.ORG_PACKAGE_WRITER_TOKEN }}" - } - } - } + "username": "${{ vars.ORG_PACKAGE_WRITER_USERNAME }}", + "password": "${{ secrets.ORG_PACKAGE_WRITER_TOKEN }}" }] - name: Make maven wrapper executable run: chmod +x mvnw + - name: Validate tag + if: startsWith(github.ref, 'refs/tags/') + run: | + TAG_NAME="${GITHUB_REF#refs/tags/}" + PROJECT_VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout) + + if [[ "$PROJECT_VERSION" != "$TAG_NAME" ]]; then + echo "::error::pom.xml version '$PROJECT_VERSION' does not match tag '$TAG_NAME'" + exit 1 + fi + - name: Download dependencies run: ./mvnw $COMMON_MAVEN_OPTS quarkus:go-offline @@ -87,5 +95,5 @@ jobs: run: ./mvnw $COMMON_MAVEN_OPTS org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=quarkus-commons -Dsonar.projectName='quarkus-commons' -Dsonar.coverage.jacoco.xmlReportPaths=../**/target/jacoco-report/jacoco.xml - name: Publish jars - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') run: ./mvnw $COMMON_MAVEN_OPTS deploy -Dmaven.test.skip=true -Dmaven.javadoc.skip=true \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 37ff2fd..3043398 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v4 - name: Retrieve project metadata - uses: https://github.com/radcortez/project-metadata-action@master + uses: https://github.com/radcortez/project-metadata-action@main id: metadata with: metadata-file-path: '.github/project.yaml' diff --git a/.github/workflows/validate-versions.yaml b/.github/workflows/validate-versions.yaml index a44b4c3..7828cbe 100644 --- a/.github/workflows/validate-versions.yaml +++ b/.github/workflows/validate-versions.yaml @@ -14,7 +14,7 @@ jobs: uses: https://github.com/actions/checkout@v4 - name: Retrieve project metadata - uses: https://github.com/radcortez/project-metadata-action@master + uses: https://github.com/radcortez/project-metadata-action@main id: metadata with: metadata-file-path: '.github/project.yaml' diff --git a/README.md b/README.md index 3f9134e..1cfb1f7 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ Quarkus Commons =============== [![Java version](https://img.shields.io/badge/Java%20version-21-brightgreen)](https://openjdk.org/projects/jdk/21/) -[![Coverage](https://sonarqube.pub.production.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=coverage&token=sqb_b39e0a05145228a10eb07d8771fd073297800645)](https://sonarqube.pub.production.kvant.cloud/dashboard?id=quarkus-commons) -[![Duplicated Lines (%)](https://sonarqube.pub.production.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=duplicated_lines_density&token=sqb_b39e0a05145228a10eb07d8771fd073297800645)](https://sonarqube.pub.production.kvant.cloud/dashboard?id=quarkus-commons) -[![Quality Gate Status](https://sonarqube.pub.production.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=alert_status&token=sqb_b39e0a05145228a10eb07d8771fd073297800645)](https://sonarqube.pub.production.kvant.cloud/dashboard?id=quarkus-commons) -[![Security Rating](https://sonarqube.pub.production.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=security_rating&token=sqb_b39e0a05145228a10eb07d8771fd073297800645)](https://sonarqube.pub.production.kvant.cloud/dashboard?id=quarkus-commons) -[![Reliability Rating](https://sonarqube.pub.production.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=reliability_rating&token=sqb_b39e0a05145228a10eb07d8771fd073297800645)](https://sonarqube.pub.production.kvant.cloud/dashboard?id=quarkus-commons) -[![Maintainability Rating](https://sonarqube.pub.production.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=sqale_rating&token=sqb_b39e0a05145228a10eb07d8771fd073297800645)](https://sonarqube.pub.production.kvant.cloud/dashboard?id=quarkus-commons) +[![Coverage](https://sonarqube.pub.basel.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=coverage&token=sqb_b56d9ea175c7f51f522ce63acd7fe7807643ac9e)](https://sonarqube.pub.basel.kvant.cloud/dashboard?id=quarkus-commons) +[![Duplicated Lines (%)](https://sonarqube.pub.basel.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=duplicated_lines_density&token=sqb_b56d9ea175c7f51f522ce63acd7fe7807643ac9e)](https://sonarqube.pub.basel.kvant.cloud/dashboard?id=quarkus-commons) +[![Quality Gate Status](https://sonarqube.pub.basel.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=alert_status&token=sqb_b56d9ea175c7f51f522ce63acd7fe7807643ac9e)](https://sonarqube.pub.basel.kvant.cloud/dashboard?id=quarkus-commons) +[![Security Rating](https://sonarqube.pub.basel.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=security_rating&token=sqb_b56d9ea175c7f51f522ce63acd7fe7807643ac9e)](https://sonarqube.pub.basel.kvant.cloud/dashboard?id=quarkus-commons) +[![Reliability Rating](https://sonarqube.pub.basel.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=reliability_rating&token=sqb_b56d9ea175c7f51f522ce63acd7fe7807643ac9e)](https://sonarqube.pub.basel.kvant.cloud/dashboard?id=quarkus-commons) +[![Maintainability Rating](https://sonarqube.pub.basel.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=sqale_rating&token=sqb_b56d9ea175c7f51f522ce63acd7fe7807643ac9e)](https://sonarqube.pub.basel.kvant.cloud/dashboard?id=quarkus-commons) # Introduction @@ -16,9 +16,11 @@ that can be used by Quarkus applications. The modules are: +* `quarkus-audit-tools` * `quarkus-clock-service` * `quarkus-json-service` * `quarkus-message-digest-service` * `quarkus-random-number-generator` +* `quarkus-tracing-service` * `quarkus-uuid-generator` diff --git a/pom.xml b/pom.xml index 1501e62..3b61944 100644 --- a/pom.xml +++ b/pom.xml @@ -3,14 +3,17 @@ 4.0.0 ch.phoenix.oss quarkus-commons - 1.0.0 + 1.0.9-SNAPSHOT pom + quarkus-audit-tools + quarkus-client-logger quarkus-clock-service quarkus-json-service quarkus-message-digest-service quarkus-random-number-generator + quarkus-tracing-service quarkus-uuid-generator @@ -18,15 +21,15 @@ quarkus-bom io.quarkus.platform - 3.22.3 + 3.25.0 3.14.0 3.5.3 - 2.44.4 - 2.66.0 + 2.46.1 + 2.72.0 0.8.13 - 3.3.1 + 3.4.0 3.1.1 3.3.1 @@ -70,7 +73,7 @@ scm:git:ssh://git@git-ssh.kvant.cloud:2222/phoenix-oss/quarkus-commons.git scm:git:ssh://git@git-ssh.kvant.cloud:2222/phoenix-oss/quarkus-commons.git https://git.kvant.cloud/phoenix-oss/quarkus-commons.git - 1.0.0 + HEAD @@ -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