Compare commits

...

66 commits
1.0.0 ... main

Author SHA1 Message Date
1e9ab13c97
ci(github-actions): simplify maven settings
All checks were successful
Build / build (push) Successful in 2m18s
2025-07-30 15:35:34 +02:00
58a53fe280
chore: prepare for next development iteration
Some checks failed
Build / build (push) Has been cancelled
2025-07-30 13:34:47 +00:00
584ffc85f2
chore: release 1.0.8
All checks were successful
Build / build (push) Successful in 2m10s
2025-07-30 13:34:45 +00:00
9621f913e3
chore: bump version to 1.0.8
All checks were successful
Release the current version / Execute the release (push) Successful in 1m46s
2025-07-30 15:33:00 +02:00
25ea6c556d
feat(deps): update quarkus.platform.version to 3.25.0
All checks were successful
Build / build (push) Successful in 2m14s
2025-07-30 15:27:53 +02:00
7a33e9cdbb
chore: prepare for next development iteration
All checks were successful
Build / build (push) Successful in 2m19s
2025-07-30 12:52:53 +00:00
6a3026de70
chore: release 1.0.7
All checks were successful
Build / build (push) Successful in 2m6s
2025-07-30 12:52:52 +00:00
8269687a6e Merge pull request 'chore: bump version to 1.0.7' (#100) from version into main
All checks were successful
Release the current version / Execute the release (push) Successful in 1m31s
Reviewed-on: #100
2025-07-30 12:51:24 +00:00
3ae16bfa25
chore: bump version to 1.0.7
All checks were successful
Validate release versions / release (pull_request) Successful in 4s
Build / build (pull_request) Successful in 2m0s
2025-07-30 14:49:00 +02:00
ee96894e87 Merge pull request 'feat(client-logger): redact headers based on configuration' (#99) from redacting into main
All checks were successful
Build / build (push) Successful in 2m9s
Reviewed-on: #99
2025-07-30 12:47:49 +00:00
21913626ad
feat(client-logger): redact headers based on configuration
All checks were successful
Build / build (pull_request) Successful in 2m9s
2025-07-30 14:44:49 +02:00
d1acb1a0ee
chore(maven): enable jacoco rules for audit and tracing modules
All checks were successful
Build / build (push) Successful in 1m57s
2025-07-24 11:53:27 +02:00
bc0110cc29
chore: more sonarqube improvements, rename tracing service method
All checks were successful
Build / build (push) Successful in 2m23s
2025-07-24 11:11:53 +02:00
f591d514ec Merge pull request 'refactor: apply some suggestions from SonarQube' (#98) from sonarqube into main
All checks were successful
Build / build (push) Successful in 2m21s
Reviewed-on: #98
2025-07-23 12:52:04 +00:00
331a830c2b
refactor: apply some suggestions from SonarQube
All checks were successful
Build / build (pull_request) Successful in 1m49s
2025-07-23 14:27:42 +02:00
7d2cda5b20
docs(README): update sonarqube badges 2025-07-22 22:36:43 +02:00
ca915c4bf5
feat(deps): update io.smallrye:jandex-maven-plugin to 3.4.0
All checks were successful
Build / build (push) Successful in 2m6s
2025-07-22 22:31:39 +02:00
cf81524d86
feat(deps): update com.diffplug.spotless:spotless-maven-plugin to 2.46.1
Some checks failed
Build / build (push) Has been cancelled
2025-07-22 22:30:56 +02:00
35d4e29a57
feat(deps): update palantir-java-format.version to 2.72.0
Some checks failed
Build / build (push) Has been cancelled
2025-07-22 22:30:44 +02:00
ed362d84b8
chore(maven): remove skip ci from release message
Some checks failed
Build / build (push) Has been cancelled
2025-07-22 22:30:15 +02:00
fc140833d5
chore: prepare for next development iteration [skip ci] 2025-07-18 12:03:03 +00:00
a0939d7729
chore: release 1.0.6
Some checks failed
Build / build (push) Failing after 1m43s
2025-07-18 12:03:02 +00:00
2d23835810
chore: bump version to 1.0.6
All checks were successful
Release the current version / Execute the release (push) Successful in 1m42s
2025-07-18 14:01:19 +02:00
5e8f8fefa8
ci(github-actions): fix jars not being published on tags
Some checks failed
Release the current version / Execute the release (push) Has been cancelled
Build / build (push) Successful in 1m44s
2025-07-18 14:01:02 +02:00
f6e85f2c00 Merge pull request 'ci(github-actions): revamp build.yaml to trigger on tags and do proper validation of version' (#96) from build into main
All checks were successful
Build / build (push) Successful in 2m7s
Reviewed-on: #96
2025-07-18 08:29:23 +00:00
a5e0c2672b
ci(github-actions): revamp build.yaml to trigger on tags and do proper validation of version
All checks were successful
Build / build (pull_request) Successful in 1m16s
2025-07-18 10:26:00 +02:00
1cc8a39c88 Merge pull request 'fix(deps): update quarkus.platform.version to 3.24.4' (#95) from quarkus-3.24.4 into main
All checks were successful
Build / build (push) Successful in 2m7s
Reviewed-on: #95
2025-07-18 08:04:57 +00:00
8aad75a493
fix(deps): update quarkus.platform.version to 3.24.4
All checks were successful
Build / build (pull_request) Successful in 2m17s
2025-07-18 09:43:50 +02:00
880df82728
chore: prepare for next development iteration [skip ci] 2025-07-12 16:29:59 +00:00
2d1f9a1417
chore: release 1.0.5
All checks were successful
Build / build (push) Successful in 1m57s
2025-07-12 16:29:58 +00:00
129fdb768f Merge pull request 'chore: bump version to 1.0.5' (#94) from version into main
All checks were successful
Release the current version / Execute the release (push) Successful in 1m30s
Reviewed-on: #94
2025-07-12 16:28:30 +00:00
6d95a3b123
chore: bump version to 1.0.5
All checks were successful
Validate release versions / release (pull_request) Successful in 4s
Build / build (pull_request) Successful in 1m56s
2025-07-12 18:26:17 +02:00
9d84bb8c5e Merge pull request 'upgrade quarkus and other dependencies' (#93) from deps into main
All checks were successful
Build / build (push) Successful in 2m10s
Reviewed-on: #93
2025-07-12 16:25:06 +00:00
0fd62decaf
feat(deps): update com.diffplug.spotless:spotless-maven-plugin to 2.45.0
All checks were successful
Build / build (pull_request) Successful in 1m59s
2025-07-12 18:20:23 +02:00
33e10b7a40
fix(deps): update palantir-java-format.version to 2.70.0 2025-07-12 18:19:18 +02:00
4d20a75f42
fix(deps): update quarkus.platform.version to 3.24.3 2025-07-12 18:18:59 +02:00
4b20f99856
chore: prepare for next development iteration [skip ci] 2025-07-04 16:47:33 +00:00
3bbdf7015a
chore: release 1.0.4
All checks were successful
Build / build (push) Successful in 1m49s
2025-07-04 16:47:32 +00:00
d7b6286546
chore: bump version to 1.0.4
Some checks failed
Build / build (push) Has been cancelled
Release the current version / Execute the release (push) Successful in 1m25s
2025-07-04 18:46:01 +02:00
cccd9dde98
fix(deps): update quarkus.platform.version to 3.24.2
All checks were successful
Build / build (push) Successful in 2m24s
2025-07-04 18:36:27 +02:00
f9734649f2 Merge pull request 'feat: add quarkus-client-logger module with RedactingClientLogger' (#91) from client-logger into main
Some checks failed
Build / build (push) Failing after 22s
Reviewed-on: #91
2025-07-04 16:35:12 +00:00
e6ec3f57f8
feat: add quarkus-client-logger module with RedactingClientLogger
Some checks failed
Build / build (pull_request) Failing after 25s
2025-07-04 18:34:48 +02:00
1f38615a15
chore: prepare for next development iteration [skip ci] 2025-06-28 03:06:34 +00:00
a22c070401
chore: release 1.0.3
All checks were successful
Build / build (push) Successful in 2m16s
2025-06-28 03:06:33 +00:00
d5053a3862
chore: bump version to 1.0.3
All checks were successful
Release the current version / Execute the release (push) Successful in 1m13s
2025-06-28 05:05:14 +02:00
a9e1f3d8fa Merge pull request 'fix(tracing): implement redact for query, add default values' (#89) from redact into main
All checks were successful
Build / build (push) Successful in 2m3s
Reviewed-on: #89
2025-06-28 02:34:16 +00:00
5ac5d90f97
fix(tracing): implement redact for query, add default values
All checks were successful
Build / build (pull_request) Successful in 1m50s
2025-06-28 04:22:36 +02:00
b813ed4347 Merge pull request 'feat(deps): update quarkus.platform.version to 3.24.1' (#88) from quarkus-3.24.1 into main
All checks were successful
Build / build (push) Successful in 1m52s
Reviewed-on: #88
2025-06-27 20:09:14 +00:00
6f7b048266
feat(deps): update quarkus.platform.version to 3.24.1
All checks were successful
Build / build (pull_request) Successful in 1m32s
2025-06-27 21:53:30 +02:00
3081156f9f
chore: prepare for next development iteration [skip ci] 2025-06-19 15:12:07 +00:00
c55e317c92
chore: release 1.0.2
All checks were successful
Build / build (push) Successful in 1m48s
2025-06-19 15:12:06 +00:00
66c190208b
chore: bump to 1.0.2
Some checks failed
Build / build (push) Has been cancelled
Release the current version / Execute the release (push) Successful in 1m9s
2025-06-19 17:10:52 +02:00
b56759c1e6 Merge pull request 'feat(clock): add local and zoned date methods to service' (#86) from clock into main
All checks were successful
Build / build (push) Successful in 3m22s
Reviewed-on: #86
2025-06-19 14:21:35 +00:00
6ac4bd783e
feat(clock): add local and zoned date methods to service
All checks were successful
Build / build (pull_request) Successful in 2m59s
2025-06-19 16:12:02 +02:00
bbd1d80d6c
chore: prepare for next development iteration [skip ci] 2025-06-18 00:18:21 +00:00
1bd06f1857
chore: release 1.0.1
All checks were successful
Build / build (push) Successful in 1m55s
2025-06-18 00:18:20 +00:00
acbe6c5ae2
chore: bump version to 1.0.1
All checks were successful
Release the current version / Execute the release (push) Successful in 1m48s
2025-06-18 02:16:31 +02:00
7c45b3e4ae
chore: bump to 1.0.1 2025-06-18 02:15:11 +02:00
2844cff58a
ci(github-actions): use project-metadata-action@main instead of master
Some checks failed
Build / build (push) Has been cancelled
Release the current version / Execute the release (push) Has been cancelled
2025-06-18 01:57:56 +02:00
a0006d4455
fix(tracing): use trace level instead of info on TracingServiceImpl
All checks were successful
Build / build (push) Successful in 2m30s
2025-06-18 01:54:21 +02:00
291e3b0712
docs(README): update modules list 2025-06-18 01:45:56 +02:00
eca733d2ce Merge pull request 'Create tracing and audit modules, upgrade quarkus to 3.23.3' (#84) from tracing-and-audit into main
All checks were successful
Build / build (push) Successful in 2m24s
Reviewed-on: #84
2025-06-17 23:44:29 +00:00
f268c4a27a
feat(audit): add quarkus-audit-tools module
All checks were successful
Build / build (pull_request) Successful in 1m57s
2025-06-18 01:41:16 +02:00
db0026b723
feat(tracing): add quarkus-tracing-service module 2025-06-18 01:40:26 +02:00
75a778296c
feat(deps): update quarkus.platform.version to 3.23.3 2025-06-18 01:39:59 +02:00
2ceace7ce7
chore: prepare for next development iteration [skip ci] 2025-05-15 14:31:36 +00:00
65 changed files with 2425 additions and 35 deletions

View file

@ -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"

View file

@ -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

View file

@ -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'

View file

@ -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'

View file

@ -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`

17
pom.xml
View file

@ -3,14 +3,17 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ch.phoenix.oss</groupId>
<artifactId>quarkus-commons</artifactId>
<version>1.0.0</version>
<version>1.0.9-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>quarkus-audit-tools</module>
<module>quarkus-client-logger</module>
<module>quarkus-clock-service</module>
<module>quarkus-json-service</module>
<module>quarkus-message-digest-service</module>
<module>quarkus-random-number-generator</module>
<module>quarkus-tracing-service</module>
<module>quarkus-uuid-generator</module>
</modules>
@ -18,15 +21,15 @@
<!-- Quarkus properties -->
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-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.25.0</quarkus.platform.version>
<!-- Plugin versions -->
<compiler-plugin.version>3.14.0</compiler-plugin.version>
<surefire-plugin.version>3.5.3</surefire-plugin.version>
<spotless-plugin.version>2.44.4</spotless-plugin.version>
<palantir-java-format.version>2.66.0</palantir-java-format.version>
<spotless-plugin.version>2.46.1</spotless-plugin.version>
<palantir-java-format.version>2.72.0</palantir-java-format.version>
<jacoco-plugin.version>0.8.13</jacoco-plugin.version> <!-- Match with version from Quarkus BOM -->
<jandex-plugin.version>3.3.1</jandex-plugin.version>
<jandex-plugin.version>3.4.0</jandex-plugin.version>
<release-plugin.version>3.1.1</release-plugin.version>
<source-plugin.version>3.3.1</source-plugin.version>
@ -70,7 +73,7 @@
<connection>scm:git:ssh://git@git-ssh.kvant.cloud:2222/phoenix-oss/quarkus-commons.git</connection>
<developerConnection>scm:git:ssh://git@git-ssh.kvant.cloud:2222/phoenix-oss/quarkus-commons.git</developerConnection>
<url>https://git.kvant.cloud/phoenix-oss/quarkus-commons.git</url>
<tag>1.0.0</tag>
<tag>HEAD</tag>
</scm>
<dependencies>
@ -193,7 +196,7 @@
<tagNameFormat>@{project.version}</tagNameFormat>
<checkModificationExcludes>mvnw</checkModificationExcludes>
<scmReleaseCommitComment>chore: release @{releaseLabel}</scmReleaseCommitComment>
<scmDevelopmentCommitComment>chore: prepare for next development iteration [skip ci]</scmDevelopmentCommitComment>
<scmDevelopmentCommitComment>chore: prepare for next development iteration</scmDevelopmentCommitComment>
<autoVersionSubmodules>true</autoVersionSubmodules>
</configuration>
</plugin>

View 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.9-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>

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
}
}

View file

@ -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 + '}';
}
}

View file

@ -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();
}

View file

@ -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());
});
}
}

View file

@ -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());
});
}
}

View file

@ -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");
}
}
}

View file

@ -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}");
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View 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

View file

@ -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;

View file

@ -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();

View 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();

View file

@ -0,0 +1,65 @@
<?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.9-SNAPSHOT</version>
</parent>
<artifactId>quarkus-client-logger</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-config-yaml</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</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>0.92</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

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

View file

@ -0,0 +1,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<String> 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<String, String> 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();
}
}

View file

@ -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<String> DEFAULT_REDACTED_HEADERS = Set.of(HttpHeaders.AUTHORIZATION.toLowerCase());
Optional<Set<@WithConverter(LowerCaseStringConverter.class) String>> redact();
}
}

View file

@ -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<String, String> getConfigOverrides() {
return Map.of("quarkus.log.category.\"ch.phoenix.oss.quarkus.commons.client.logger\".level", "INFO");
}
}

View file

@ -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", "");
}
}

View file

@ -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", "");
}
}

View file

@ -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<String, String> getConfigOverrides() {
return Map.of("quarkus.rest-client.logging.scope", "none");
}
}

View file

@ -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", "");
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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

View file

@ -5,7 +5,7 @@
<parent>
<groupId>ch.phoenix.oss</groupId>
<artifactId>quarkus-commons</artifactId>
<version>1.0.0</version>
<version>1.0.9-SNAPSHOT</version>
</parent>
<artifactId>quarkus-clock-service</artifactId>

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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]"));
}
}

View file

@ -5,7 +5,7 @@
<parent>
<groupId>ch.phoenix.oss</groupId>
<artifactId>quarkus-commons</artifactId>
<version>1.0.0</version>
<version>1.0.9-SNAPSHOT</version>
</parent>
<artifactId>quarkus-json-service</artifactId>

View file

@ -5,7 +5,7 @@
<parent>
<groupId>ch.phoenix.oss</groupId>
<artifactId>quarkus-commons</artifactId>
<version>1.0.0</version>
<version>1.0.9-SNAPSHOT</version>
</parent>
<artifactId>quarkus-message-digest-service</artifactId>

View file

@ -5,7 +5,7 @@
<parent>
<groupId>ch.phoenix.oss</groupId>
<artifactId>quarkus-commons</artifactId>
<version>1.0.0</version>
<version>1.0.9-SNAPSHOT</version>
</parent>
<artifactId>quarkus-random-number-generator</artifactId>

View file

@ -0,0 +1,78 @@
<?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.9-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>
<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>0.95</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

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

View file

@ -0,0 +1,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<String> 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<Set<@WithConverter(LowerCaseStringConverter.class) String>> redact();
}
Path path();
interface Path {
@WithDefault("false")
boolean includeRaw();
}
Query query();
interface Query {
Set<String> 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<Set<@WithConverter(LowerCaseStringConverter.class) String>> redact();
}
}
}

View file

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

View file

@ -0,0 +1,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<String> value) {
return String.join(", ", value);
}
private String getRoutePattern(ResourceInfo resourceInfo) {
String classPath = getPathValue(resourceInfo.getResourceClass().getAnnotation(Path.class));
String methodPath = getPathValue(resourceInfo.getResourceMethod().getAnnotation(Path.class));
if (!classPath.isEmpty()) {
if (methodPath.isEmpty()) {
return "/" + classPath;
} else {
return "/" + classPath + "/" + methodPath;
}
} else {
if (methodPath.isEmpty()) {
return "/";
} else {
return "/" + methodPath;
}
}
}
private static String getPathValue(Path path) {
if (path == null) {
return "";
}
return trimSlashes(path.value());
}
private static String trimSlashes(String segment) {
if (segment.isEmpty() || "/".equals(segment)) {
return "";
}
// Assuming that it's not possible for a segment to contain //,
// thus it's possible to avoid extra ifs and whiles, as well
// as the use of regexes
int start = (segment.charAt(0) == '/') ? 1 : 0;
int end = (segment.charAt(segment.length() - 1) == '/') ? segment.length() - 1 : segment.length();
return segment.substring(start, end);
}
}

View file

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

View file

@ -0,0 +1,69 @@
package ch.phoenix.oss.quarkus.commons.tracing;
import io.opentelemetry.api.trace.Span;
import io.quarkus.arc.DefaultBean;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.MDC;
@DefaultBean
@ApplicationScoped
class TracingServiceImpl implements TracingService {
private final Span span;
TracingServiceImpl(Span span) {
this.span = span;
}
@Override
public void clearAll() {
MDC.clear();
}
@Override
public void trace(final String key, final Object value) {
Log.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);
}
}

View file

@ -0,0 +1,44 @@
package ch.phoenix.oss.quarkus.commons.tracing;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectSpy;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
@QuarkusTest
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);
}
}

View file

@ -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);
}
}

View file

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

View file

@ -0,0 +1,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);
}
}

View file

@ -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<Arguments> 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<String, String> 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);
}
}

View file

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

View file

@ -0,0 +1,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();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,23 @@
package ch.phoenix.oss.quarkus.commons.tracing.resource;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/")
@Produces(MediaType.TEXT_PLAIN)
public class SlashResource {
@GET
@Path("/leading/{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";
}
}

View file

@ -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

View file

@ -5,7 +5,7 @@
<parent>
<groupId>ch.phoenix.oss</groupId>
<artifactId>quarkus-commons</artifactId>
<version>1.0.0</version>
<version>1.0.9-SNAPSHOT</version>
</parent>
<artifactId>quarkus-uuid-generator</artifactId>