Create tracing and audit modules, upgrade quarkus to 3.23.3 #84
37 changed files with 1702 additions and 1 deletions
4
pom.xml
4
pom.xml
|
@ -7,10 +7,12 @@
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
|
<module>quarkus-audit-tools</module>
|
||||||
<module>quarkus-clock-service</module>
|
<module>quarkus-clock-service</module>
|
||||||
<module>quarkus-json-service</module>
|
<module>quarkus-json-service</module>
|
||||||
<module>quarkus-message-digest-service</module>
|
<module>quarkus-message-digest-service</module>
|
||||||
<module>quarkus-random-number-generator</module>
|
<module>quarkus-random-number-generator</module>
|
||||||
|
<module>quarkus-tracing-service</module>
|
||||||
<module>quarkus-uuid-generator</module>
|
<module>quarkus-uuid-generator</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
|
@ -18,7 +20,7 @@
|
||||||
<!-- Quarkus properties -->
|
<!-- Quarkus properties -->
|
||||||
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
||||||
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
|
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
|
||||||
<quarkus.platform.version>3.22.3</quarkus.platform.version>
|
<quarkus.platform.version>3.23.3</quarkus.platform.version>
|
||||||
|
|
||||||
<!-- Plugin versions -->
|
<!-- Plugin versions -->
|
||||||
<compiler-plugin.version>3.14.0</compiler-plugin.version>
|
<compiler-plugin.version>3.14.0</compiler-plugin.version>
|
||||||
|
|
88
quarkus-audit-tools/pom.xml
Normal file
88
quarkus-audit-tools/pom.xml
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>ch.phoenix.oss</groupId>
|
||||||
|
<artifactId>quarkus-commons</artifactId>
|
||||||
|
<version>1.0.1-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>quarkus-audit-tools</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.phoenix.oss</groupId>
|
||||||
|
<artifactId>quarkus-tracing-service</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-hibernate-envers</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-hibernate-orm-panache</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-jdbc-postgresql</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-flyway</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-flyway-postgresql</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-elytron-security-properties-file</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-config-yaml</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<!-- <build>-->
|
||||||
|
<!-- <plugins>-->
|
||||||
|
<!-- <plugin>-->
|
||||||
|
<!-- <groupId>org.jacoco</groupId>-->
|
||||||
|
<!-- <artifactId>jacoco-maven-plugin</artifactId>-->
|
||||||
|
<!-- <version>${jacoco-plugin.version}</version>-->
|
||||||
|
<!-- <executions>-->
|
||||||
|
<!-- <execution>-->
|
||||||
|
<!-- <id>jacoco-check</id>-->
|
||||||
|
<!-- <goals>-->
|
||||||
|
<!-- <goal>check</goal>-->
|
||||||
|
<!-- </goals>-->
|
||||||
|
<!-- <phase>test</phase>-->
|
||||||
|
<!-- <configuration>-->
|
||||||
|
<!-- <dataFile>${project.build.directory}/jacoco-quarkus.exec</dataFile>-->
|
||||||
|
<!-- <rules>-->
|
||||||
|
<!-- <rule>-->
|
||||||
|
<!-- <element>BUNDLE</element>-->
|
||||||
|
<!-- <limits>-->
|
||||||
|
<!-- <limit>-->
|
||||||
|
<!-- <counter>INSTRUCTION</counter>-->
|
||||||
|
<!-- <value>COVEREDRATIO</value>-->
|
||||||
|
<!-- <minimum>1</minimum>-->
|
||||||
|
<!-- </limit>-->
|
||||||
|
<!-- </limits>-->
|
||||||
|
<!-- </rule>-->
|
||||||
|
<!-- </rules>-->
|
||||||
|
<!-- </configuration>-->
|
||||||
|
<!-- </execution>-->
|
||||||
|
<!-- </executions>-->
|
||||||
|
<!-- </plugin>-->
|
||||||
|
<!-- </plugins>-->
|
||||||
|
<!-- </build>-->
|
||||||
|
|
||||||
|
</project>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + '}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.audit;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.audit;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
assertThat(r1).as("Revisions should be equal").isEqualTo(r2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHashCode() {}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testToString() {}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
22
quarkus-audit-tools/src/test/resources/application.yaml
Normal file
22
quarkus-audit-tools/src/test/resources/application.yaml
Normal file
|
@ -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
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
37
quarkus-audit-tools/src/test/resources/import.sql
Normal file
37
quarkus-audit-tools/src/test/resources/import.sql
Normal file
|
@ -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();
|
44
quarkus-tracing-service/pom.xml
Normal file
44
quarkus-tracing-service/pom.xml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>ch.phoenix.oss</groupId>
|
||||||
|
<artifactId>quarkus-commons</artifactId>
|
||||||
|
<version>1.0.1-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>quarkus-tracing-service</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-rest-jackson</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-opentelemetry</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-elytron-security-properties-file</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-config-yaml</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.rest-assured</groupId>
|
||||||
|
<artifactId>rest-assured</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,10 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.tracing;
|
||||||
|
|
||||||
|
import org.eclipse.microprofile.config.spi.Converter;
|
||||||
|
|
||||||
|
public class LowerCaseStringConverter implements Converter<String> {
|
||||||
|
@Override
|
||||||
|
public String convert(String value) throws IllegalArgumentException, NullPointerException {
|
||||||
|
return value.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.tracing;
|
||||||
|
|
||||||
|
import io.smallrye.config.ConfigMapping;
|
||||||
|
import io.smallrye.config.WithConverter;
|
||||||
|
import io.smallrye.config.WithDefault;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ConfigMapping(prefix = "phoenix.commons.tracing")
|
||||||
|
public interface TracingConfiguration {
|
||||||
|
|
||||||
|
RequestFilterConfiguration requestFilter();
|
||||||
|
|
||||||
|
interface RequestFilterConfiguration {
|
||||||
|
|
||||||
|
Headers headers();
|
||||||
|
|
||||||
|
interface Headers {
|
||||||
|
|
||||||
|
Optional<Set<@WithConverter(LowerCaseStringConverter.class) String>> redact();
|
||||||
|
}
|
||||||
|
|
||||||
|
Path path();
|
||||||
|
|
||||||
|
interface Path {
|
||||||
|
|
||||||
|
@WithDefault("false")
|
||||||
|
boolean includeRaw();
|
||||||
|
}
|
||||||
|
|
||||||
|
Query query();
|
||||||
|
|
||||||
|
interface Query {
|
||||||
|
@WithDefault("false")
|
||||||
|
boolean includeRaw();
|
||||||
|
|
||||||
|
Optional<Set<@WithConverter(LowerCaseStringConverter.class) String>> redact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.tracing;
|
||||||
|
|
||||||
|
import io.quarkus.arc.Unremovable;
|
||||||
|
import io.quarkus.logging.Log;
|
||||||
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
|
import io.vertx.ext.web.RoutingContext;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||||
|
import jakarta.ws.rs.container.ResourceInfo;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.jboss.resteasy.reactive.server.ServerRequestFilter;
|
||||||
|
|
||||||
|
@Unremovable
|
||||||
|
public class TracingRequestFilter {
|
||||||
|
|
||||||
|
private final RoutingContext routingContext;
|
||||||
|
private final TracingService tracingService;
|
||||||
|
private final SecurityIdentity securityIdentity;
|
||||||
|
private final TracingConfiguration configuration;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TracingRequestFilter(
|
||||||
|
RoutingContext routingContext,
|
||||||
|
TracingService tracingService,
|
||||||
|
SecurityIdentity securityIdentity,
|
||||||
|
TracingConfiguration configuration) {
|
||||||
|
this.routingContext = routingContext;
|
||||||
|
this.tracingService = tracingService;
|
||||||
|
this.securityIdentity = securityIdentity;
|
||||||
|
this.configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ServerRequestFilter
|
||||||
|
public void filter(ContainerRequestContext requestContext, ResourceInfo resourceInfo) {
|
||||||
|
if (securityIdentity.isAnonymous()) {
|
||||||
|
tracingService.trace(TracingConstants.ACTOR, TracingConstants.ANONYMOUS);
|
||||||
|
} else {
|
||||||
|
tracingService.trace(
|
||||||
|
TracingConstants.ACTOR, securityIdentity.getPrincipal().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
var method = requestContext.getMethod();
|
||||||
|
tracingService.trace(TracingConstants.REQUEST_METHOD, method);
|
||||||
|
|
||||||
|
var routePattern = getRoutePattern(resourceInfo);
|
||||||
|
tracingService.trace(TracingConstants.REQUEST_ROUTE, routePattern);
|
||||||
|
|
||||||
|
var uriInfo = requestContext.getUriInfo();
|
||||||
|
uriInfo.getPathParameters()
|
||||||
|
.forEach((key, value) ->
|
||||||
|
tracingService.trace(TracingConstants.REQUEST_PATH_PARAMS + '.' + key, joinStrings(value)));
|
||||||
|
|
||||||
|
if (configuration.requestFilter().path().includeRaw()) {
|
||||||
|
tracingService.trace(
|
||||||
|
TracingConstants.REQUEST_PATH_RAW, uriInfo.getAbsolutePath().getRawPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
requestContext.getHeaders().forEach((key, value) -> {
|
||||||
|
var lowerCaseKey = key.toLowerCase();
|
||||||
|
var property = TracingConstants.REQUEST_HEADERS + '.' + lowerCaseKey;
|
||||||
|
if (configuration
|
||||||
|
.requestFilter()
|
||||||
|
.headers()
|
||||||
|
.redact()
|
||||||
|
.orElse(Set.of())
|
||||||
|
.contains(lowerCaseKey)) {
|
||||||
|
tracingService.trace(property, "********");
|
||||||
|
} else {
|
||||||
|
tracingService.trace(property, joinStrings(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
uriInfo.getQueryParameters()
|
||||||
|
.forEach((key, value) ->
|
||||||
|
tracingService.trace(TracingConstants.REQUEST_QUERY_PARAMS + '.' + key, joinStrings(value)));
|
||||||
|
|
||||||
|
if (configuration.requestFilter().query().includeRaw()) {
|
||||||
|
var rawQuery = uriInfo.getRequestUri().getRawQuery();
|
||||||
|
if (rawQuery != null && !rawQuery.isBlank()) {
|
||||||
|
tracingService.trace(TracingConstants.REQUEST_QUERY_RAW, rawQuery);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracingService.trace(
|
||||||
|
TracingConstants.REQUEST_CLIENT_IP,
|
||||||
|
routingContext.request().connection().remoteAddress(true).hostAddress());
|
||||||
|
|
||||||
|
if (Log.isTraceEnabled()) {
|
||||||
|
Log.tracef(
|
||||||
|
"Incoming request: %s %s", method, uriInfo.getAbsolutePath().getRawPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String joinStrings(List<String> value) {
|
||||||
|
return String.join(", ", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getRoutePattern(ResourceInfo resourceInfo) {
|
||||||
|
String classPath = getPathValue(resourceInfo.getResourceClass().getAnnotation(Path.class));
|
||||||
|
String methodPath = getPathValue(resourceInfo.getResourceMethod().getAnnotation(Path.class));
|
||||||
|
|
||||||
|
if (!classPath.isEmpty()) {
|
||||||
|
if (methodPath.isEmpty()) {
|
||||||
|
return "/" + classPath;
|
||||||
|
} else {
|
||||||
|
return "/" + classPath + "/" + methodPath;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (methodPath.isEmpty()) {
|
||||||
|
return "/";
|
||||||
|
} else {
|
||||||
|
return "/" + methodPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getPathValue(Path path) {
|
||||||
|
if (path == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return trimSlashes(path.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String trimSlashes(String segment) {
|
||||||
|
if (segment.isEmpty() || "/".equals(segment)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming that it's not possible for a segment to contain //,
|
||||||
|
// thus it's possible to avoid extra ifs and whiles, as well
|
||||||
|
// as the use of regexes
|
||||||
|
int start = (segment.charAt(0) == '/') ? 1 : 0;
|
||||||
|
int end = (segment.charAt(segment.length() - 1) == '/') ? segment.length() - 1 : segment.length();
|
||||||
|
|
||||||
|
return segment.substring(start, end);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.tracing;
|
||||||
|
|
||||||
|
public interface TracingService {
|
||||||
|
|
||||||
|
void clearAll();
|
||||||
|
|
||||||
|
void trace(String key, Object value);
|
||||||
|
|
||||||
|
String getActor();
|
||||||
|
|
||||||
|
String getRequestPath();
|
||||||
|
|
||||||
|
String getRequestMethod();
|
||||||
|
|
||||||
|
String getRequestId();
|
||||||
|
|
||||||
|
String getTraceId();
|
||||||
|
|
||||||
|
String getSpanId();
|
||||||
|
|
||||||
|
String getClientIp();
|
||||||
|
|
||||||
|
String getSchedulerJob();
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.tracing;
|
||||||
|
|
||||||
|
import io.opentelemetry.api.trace.Span;
|
||||||
|
import io.quarkus.arc.DefaultBean;
|
||||||
|
import io.quarkus.logging.Log;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import org.jboss.logging.MDC;
|
||||||
|
|
||||||
|
@DefaultBean
|
||||||
|
@ApplicationScoped
|
||||||
|
class TracingServiceImpl implements TracingService {
|
||||||
|
|
||||||
|
private final Span span;
|
||||||
|
|
||||||
|
TracingServiceImpl(Span span) {
|
||||||
|
this.span = span;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearAll() {
|
||||||
|
MDC.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void trace(final String key, final Object value) {
|
||||||
|
Log.infof("tracing key=%s value=%s", key, value);
|
||||||
|
MDC.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getActor() {
|
||||||
|
return (String) MDC.get(TracingConstants.ACTOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRequestPath() {
|
||||||
|
return (String) MDC.get(TracingConstants.REQUEST_PATH_RAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRequestMethod() {
|
||||||
|
return (String) MDC.get(TracingConstants.REQUEST_METHOD);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRequestId() {
|
||||||
|
return (String) MDC.get(TracingConstants.REQUEST_HEADERS + ".x-request-id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTraceId() {
|
||||||
|
return span.getSpanContext().getTraceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSpanId() {
|
||||||
|
return span.getSpanContext().getSpanId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientIp() {
|
||||||
|
return (String) MDC.get(TracingConstants.REQUEST_CLIENT_IP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSchedulerJob() {
|
||||||
|
return (String) MDC.get(TracingConstants.SCHEDULER_JOB_NAME);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.tracing;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.startsWith;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.quarkus.test.junit.mockito.InjectSpy;
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
public class ActorTest {
|
||||||
|
|
||||||
|
@InjectSpy
|
||||||
|
TracingService tracingService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAuthenticated() {
|
||||||
|
var route = "/authenticated";
|
||||||
|
RestAssured.given()
|
||||||
|
.auth()
|
||||||
|
.basic("jon", "doe")
|
||||||
|
.accept(ContentType.TEXT)
|
||||||
|
.when()
|
||||||
|
.get(route)
|
||||||
|
.then()
|
||||||
|
.statusCode(200);
|
||||||
|
|
||||||
|
verify(tracingService).trace("actor", "jon");
|
||||||
|
verify(tracingService).trace("request.method", "GET");
|
||||||
|
verify(tracingService).trace("request.route", route);
|
||||||
|
verify(tracingService).trace("request.headers.accept", "text/plain");
|
||||||
|
verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate");
|
||||||
|
verify(tracingService).trace("request.headers.authorization", "Basic am9uOmRvZQ==");
|
||||||
|
verify(tracingService).trace("request.headers.connection", "Keep-Alive");
|
||||||
|
verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:"));
|
||||||
|
verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient"));
|
||||||
|
verify(tracingService).trace("request.client.ip", "127.0.0.1");
|
||||||
|
verifyNoMoreInteractions(tracingService);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.tracing;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.startsWith;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.quarkus.test.junit.TestProfile;
|
||||||
|
import io.quarkus.test.junit.mockito.InjectSpy;
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
@TestProfile(Test2Profile.class)
|
||||||
|
public class RawPathTest {
|
||||||
|
|
||||||
|
@InjectSpy
|
||||||
|
TracingService tracingService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWithRawPath() {
|
||||||
|
var route = "/no-leading-and-trailing/{param}/{param2}";
|
||||||
|
RestAssured.given()
|
||||||
|
.accept(ContentType.TEXT)
|
||||||
|
.when()
|
||||||
|
.get(route, 1, 2)
|
||||||
|
.then()
|
||||||
|
.statusCode(200);
|
||||||
|
|
||||||
|
verify(tracingService).trace("actor", "anonymous");
|
||||||
|
verify(tracingService).trace("request.method", "GET");
|
||||||
|
verify(tracingService).trace("request.route", route);
|
||||||
|
verify(tracingService).trace("request.path.params.param", "1");
|
||||||
|
verify(tracingService).trace("request.path.params.param2", "2");
|
||||||
|
verify(tracingService).trace("request.path.raw", "/no-leading-and-trailing/1/2");
|
||||||
|
verify(tracingService).trace("request.headers.accept", "text/plain");
|
||||||
|
verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate");
|
||||||
|
verify(tracingService).trace("request.headers.connection", "Keep-Alive");
|
||||||
|
verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:"));
|
||||||
|
verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient"));
|
||||||
|
verify(tracingService).trace("request.client.ip", "127.0.0.1");
|
||||||
|
verifyNoMoreInteractions(tracingService);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.tracing;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.startsWith;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.quarkus.test.junit.mockito.InjectSpy;
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
class RoutePatternTest {
|
||||||
|
|
||||||
|
@InjectSpy
|
||||||
|
TracingService tracingService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getBlankResource() {
|
||||||
|
var route = "/";
|
||||||
|
RestAssured.given().accept(ContentType.TEXT).when().get(route).then().statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void postBlankResource() {
|
||||||
|
var route = "/";
|
||||||
|
RestAssured.given().accept(ContentType.TEXT).when().post(route).then().statusCode(200);
|
||||||
|
|
||||||
|
verify(tracingService).trace("actor", "anonymous");
|
||||||
|
verify(tracingService).trace("request.method", "POST");
|
||||||
|
verify(tracingService).trace("request.route", route);
|
||||||
|
verify(tracingService).trace("request.headers.accept", "text/plain");
|
||||||
|
verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate");
|
||||||
|
verify(tracingService).trace("request.headers.connection", "Keep-Alive");
|
||||||
|
verify(tracingService).trace("request.headers.content-length", "0");
|
||||||
|
verify(tracingService)
|
||||||
|
.trace("request.headers.content-type", "application/x-www-form-urlencoded; charset=ISO-8859-1");
|
||||||
|
verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:"));
|
||||||
|
verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient"));
|
||||||
|
verify(tracingService).trace("request.client.ip", "127.0.0.1");
|
||||||
|
verifyNoMoreInteractions(tracingService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getLeadingResource() {
|
||||||
|
var route = "/leading/{id}/{anotherId}";
|
||||||
|
RestAssured.given()
|
||||||
|
.accept(ContentType.TEXT)
|
||||||
|
.when()
|
||||||
|
.get(route, 1, 2)
|
||||||
|
.then()
|
||||||
|
.statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of("id", "1", "anotherId", "2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getTrailingResource() {
|
||||||
|
var route = "/{id}/{anotherId}/trailing";
|
||||||
|
RestAssured.given()
|
||||||
|
.accept(ContentType.TEXT)
|
||||||
|
.when()
|
||||||
|
.get(route, 1, 2)
|
||||||
|
.then()
|
||||||
|
.statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of("id", "1", "anotherId", "2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getLeadingAndNoTrailingResource() {
|
||||||
|
var route = "/leading-and-no-trailing";
|
||||||
|
RestAssured.given().accept(ContentType.TEXT).when().get(route).then().statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getLeadingAndNoTrailingWithSingleParamResource() {
|
||||||
|
var route = "/leading-and-no-trailing/{param}";
|
||||||
|
RestAssured.given().accept(ContentType.TEXT).when().get(route, 1).then().statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of("param", "1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getLeadingAndNoTrailingWithMultiParamResource() {
|
||||||
|
var route = "/leading-and-no-trailing/{param}/{param2}";
|
||||||
|
RestAssured.given()
|
||||||
|
.accept(ContentType.TEXT)
|
||||||
|
.when()
|
||||||
|
.get(route, 1, 2)
|
||||||
|
.then()
|
||||||
|
.statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of("param", "1", "param2", "2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getLeadingAndTrailingResource() {
|
||||||
|
var route = "/leading-and-trailing";
|
||||||
|
RestAssured.given().accept(ContentType.TEXT).when().get(route).then().statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getLeadingAndTrailingWithSingleParamResource() {
|
||||||
|
var route = "/leading-and-trailing/{param}";
|
||||||
|
RestAssured.given().accept(ContentType.TEXT).when().get(route, 1).then().statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of("param", "1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getLeadingAndTrailingWithMultiParamResource() {
|
||||||
|
var route = "/leading-and-trailing/{param}/{param2}";
|
||||||
|
RestAssured.given()
|
||||||
|
.accept(ContentType.TEXT)
|
||||||
|
.when()
|
||||||
|
.get(route, 1, 2)
|
||||||
|
.then()
|
||||||
|
.statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of("param", "1", "param2", "2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNoLeadingAndNoTrailingResource() {
|
||||||
|
var route = "/no-leading-and-no-trailing";
|
||||||
|
RestAssured.given().accept(ContentType.TEXT).when().get(route).then().statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void geNoLeadingAndNoTrailingWithSingleParamResource() {
|
||||||
|
var route = "/no-leading-and-no-trailing/{param}";
|
||||||
|
RestAssured.given().accept(ContentType.TEXT).when().get(route, 1).then().statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of("param", "1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNoLeadingAndNoTrailingWithMultiParamResource() {
|
||||||
|
var route = "/no-leading-and-no-trailing/{param}/{param2}";
|
||||||
|
RestAssured.given()
|
||||||
|
.accept(ContentType.TEXT)
|
||||||
|
.when()
|
||||||
|
.get(route, 1, 2)
|
||||||
|
.then()
|
||||||
|
.statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of("param", "1", "param2", "2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNoLeadingAndTrailingResource() {
|
||||||
|
var route = "/no-leading-and-trailing";
|
||||||
|
RestAssured.given().accept(ContentType.TEXT).when().get(route).then().statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNoLeadingAndTrailingWithSingleParamResource() {
|
||||||
|
var route = "/no-leading-and-trailing/{param}";
|
||||||
|
RestAssured.given().accept(ContentType.TEXT).when().get(route, 1).then().statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of("param", "1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNoLeadingAndTrailingWithMultiParamResource() {
|
||||||
|
var route = "/no-leading-and-trailing/{param}/{param2}";
|
||||||
|
RestAssured.given()
|
||||||
|
.accept(ContentType.TEXT)
|
||||||
|
.when()
|
||||||
|
.get(route, 1, 2)
|
||||||
|
.then()
|
||||||
|
.statusCode(200);
|
||||||
|
|
||||||
|
verifyGetTracing(route, Map.of("param", "1", "param2", "2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyGetTracing(String route, Map<String, String> pathParams) {
|
||||||
|
verify(tracingService).trace("actor", "anonymous");
|
||||||
|
verify(tracingService).trace("request.method", "GET");
|
||||||
|
verify(tracingService).trace("request.route", route);
|
||||||
|
pathParams.forEach((key, value) -> verify(tracingService).trace("request.path.params." + key, value));
|
||||||
|
verify(tracingService).trace("request.headers.accept", "text/plain");
|
||||||
|
verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate");
|
||||||
|
verify(tracingService).trace("request.headers.connection", "Keep-Alive");
|
||||||
|
verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:"));
|
||||||
|
verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient"));
|
||||||
|
verify(tracingService).trace("request.client.ip", "127.0.0.1");
|
||||||
|
verifyNoMoreInteractions(tracingService);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.tracing.resource;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
@Path("/")
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
public class SlashResource {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/leading/{id}/{anotherId}")
|
||||||
|
public String doubleLeading(int id, int anotherId) {
|
||||||
|
return "leading/" + id + "/" + anotherId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{id}/{anotherId}/trailing/")
|
||||||
|
public String doubleTrailing(int id, int anotherId) {
|
||||||
|
return id + "/" + anotherId + "/trailing";
|
||||||
|
}
|
||||||
|
}
|
38
quarkus-tracing-service/src/test/resources/application.yaml
Normal file
38
quarkus-tracing-service/src/test/resources/application.yaml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
quarkus:
|
||||||
|
http:
|
||||||
|
test-port: 0
|
||||||
|
log:
|
||||||
|
category:
|
||||||
|
"org.apache.http.wire":
|
||||||
|
level: DEBUG # set to DEBUG when RestAssured logs are necessary to understand test failures
|
||||||
|
otel:
|
||||||
|
traces:
|
||||||
|
exporter: none
|
||||||
|
security:
|
||||||
|
users:
|
||||||
|
embedded:
|
||||||
|
enabled: true
|
||||||
|
plain-text: true
|
||||||
|
users:
|
||||||
|
jon: doe
|
||||||
|
|
||||||
|
"%test2":
|
||||||
|
quarkus:
|
||||||
|
log:
|
||||||
|
min-level: TRACE
|
||||||
|
category:
|
||||||
|
"ch.phoenix.oss.quarkus.commons.tracing":
|
||||||
|
level: TRACE
|
||||||
|
phoenix:
|
||||||
|
commons:
|
||||||
|
tracing:
|
||||||
|
request-filter:
|
||||||
|
path:
|
||||||
|
include-raw: true
|
||||||
|
headers:
|
||||||
|
redact:
|
||||||
|
- AUTHORIZATION
|
||||||
|
query:
|
||||||
|
include-raw: true
|
||||||
|
redact:
|
||||||
|
- ACCESS_TOKEN
|
Loading…
Add table
Add a link
Reference in a new issue