diff --git a/pom.xml b/pom.xml
index 1d03e52..2a94134 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,10 +7,12 @@
+ * 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..5ad9ff7
--- /dev/null
+++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProviderTest.java
@@ -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");
+ }
+ }
+}
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..1b08c56
--- /dev/null
+++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/RevisionTest.java
@@ -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() {}
+}
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-tracing-service/pom.xml b/quarkus-tracing-service/pom.xml
new file mode 100644
index 0000000..b95f87d
--- /dev/null
+++ b/quarkus-tracing-service/pom.xml
@@ -0,0 +1,44 @@
+
+