From af982346c80404bdefd545ef9d3564786181159f Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 12 May 2026 13:30:44 +0200 Subject: [PATCH 01/44] start work on request-uuid extration --- .../main/java/de/ii/xtraplatform/base/domain/LogContext.java | 3 ++- .../java/de/ii/xtraplatform/services/app/ServicesEndpoint.java | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/LogContext.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/LogContext.java index ecd9e7e6..03353a34 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/LogContext.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/LogContext.java @@ -36,7 +36,8 @@ private LogContext() { public enum CONTEXT { SERVICE, - REQUEST + REQUEST, + AUDIT } public enum MARKER implements MyMarker { diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java index cf9b0d2b..67d96aa0 100755 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java @@ -360,6 +360,7 @@ private void setupRequestLogging( String serviceId, Integer version, ContainerRequestContext containerRequestContext) { String uuid = LogContext.generateRandomUuid().toString(); containerRequestContext.setProperty("REQUEST_ID", uuid); + LogContext.put(LogContext.CONTEXT.AUDIT, uuid); if (LOGGER.isDebugEnabled() || LOGGER.isDebugEnabled(MARKER.REQUEST)) { LogContext.put(LogContext.CONTEXT.REQUEST, uuid); From 996884e1378d73f385fea30d5be769da43ad5b87 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Fri, 15 May 2026 16:29:10 +0200 Subject: [PATCH 02/44] finish AuditLogger foundation --- .../base/app/AuditLoggerImpl.java | 94 +++++++++++++++++++ .../xtraplatform/base/domain/AuditLogger.java | 32 +++++++ 2 files changed, 126 insertions(+) create mode 100644 xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java create mode 100644 xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java new file mode 100644 index 00000000..5ad5f533 --- /dev/null +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java @@ -0,0 +1,94 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.base.app; + +import com.github.azahnen.dagger.annotations.AutoBind; +import de.ii.xtraplatform.base.domain.AuditLogger; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Singleton +@AutoBind +public class AuditLoggerImpl implements AuditLogger { + private final Map auditLogMapping = new ConcurrentHashMap<>(); + + @Inject + AuditLoggerImpl() {} + + private AuditLog lazyInitOrGetAuditLog(String requestUuid) { + return auditLogMapping.computeIfAbsent(requestUuid, k -> new AuditLogImpl()); + } + + @Override + public void initUser(String requestUuid, String user) { + lazyInitOrGetAuditLog(requestUuid).initUser(user); + } + + @Override + public void initType(String requestUuid, String type) { + lazyInitOrGetAuditLog(requestUuid).initType(type); + } + + @Override + public void initPropertyToValueTrack(String requestUuid, String property) { + lazyInitOrGetAuditLog(requestUuid).initPropertyToValueTrack(property); + } + + @Override + public void appendPropertyValue(String requestUuid, String property, String value) { + lazyInitOrGetAuditLog(requestUuid).appendPropertyValue(property, value); + } + + @Override + public void saveToFileAndRemove(String requestUuid) { + // ToDo Implement + } + + private static class AuditLogImpl implements AuditLogger.AuditLog { + private String user; + private String type; + private final Map> valueLog = new LinkedHashMap<>(); + + AuditLogImpl() {} + + @Override + public void initUser(String user) { + // ToDo Safety checks + this.user = user; + } + + @Override + public void initType(String type) { + // ToDo Safety checks + this.type = type; + } + + @Override + public void initPropertyToValueTrack(String property) { + valueLog.computeIfAbsent(property, p -> new LinkedHashSet<>()); + } + + @Override + public void appendPropertyValue(String property, String value) { + if (valueLog.containsKey(property)) { + valueLog.get(property).add(value); + } + } + + @Override + public String toJson() { + // ToDo implement + return user + type; + } + } +} diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java new file mode 100644 index 00000000..139c5e0a --- /dev/null +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java @@ -0,0 +1,32 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.base.domain; + +public interface AuditLogger { + interface AuditLog { + void initUser(String user); + + void initType(String type); + + void initPropertyToValueTrack(String property); + + void appendPropertyValue(String property, String value); + + String toJson(); + } + + void initUser(String requestUuid, String user); + + void initType(String requestUuid, String type); + + void initPropertyToValueTrack(String requestUuid, String property); + + void appendPropertyValue(String requestUuid, String property, String value); + + void saveToFileAndRemove(String requestUuid); +} From bbd2810bbf0e1513702e7cd6af4f7596cc3ead07 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Mon, 18 May 2026 10:32:27 +0200 Subject: [PATCH 03/44] add support for access-logging --- .../base/app/AuditLoggerImpl.java | 40 ++++++++++++++++++- .../xtraplatform/base/domain/AuditLogger.java | 8 ++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java index 5ad5f533..9a5a0c4e 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java @@ -16,12 +16,16 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Singleton @AutoBind public class AuditLoggerImpl implements AuditLogger { private final Map auditLogMapping = new ConcurrentHashMap<>(); + private static final Logger LOGGER = LoggerFactory.getLogger(AuditLoggerImpl.class); + @Inject AuditLoggerImpl() {} @@ -49,15 +53,29 @@ public void appendPropertyValue(String requestUuid, String property, String valu lazyInitOrGetAuditLog(requestUuid).appendPropertyValue(property, value); } + @Override + public void initPropertyToAccessTrack(String requestUuid, String property) { + lazyInitOrGetAuditLog(requestUuid).initPropertyToAccessTrack(property); + } + + @Override + public void markAccessed(String requestUuid, String property) { + lazyInitOrGetAuditLog(requestUuid).markAccessed(property); + } + @Override public void saveToFileAndRemove(String requestUuid) { // ToDo Implement + if (LOGGER.isInfoEnabled()) { + LOGGER.info(lazyInitOrGetAuditLog(requestUuid).toJson()); + } } private static class AuditLogImpl implements AuditLogger.AuditLog { private String user; private String type; private final Map> valueLog = new LinkedHashMap<>(); + private final Map accessLog = new LinkedHashMap<>(); AuditLogImpl() {} @@ -85,10 +103,30 @@ public void appendPropertyValue(String property, String value) { } } + @Override + public void initPropertyToAccessTrack(String property) { + accessLog.computeIfAbsent(property, p -> false); + } + + @Override + public void markAccessed(String property) { + if (accessLog.containsKey(property)) { + accessLog.put(property, true); + } + } + @Override public String toJson() { // ToDo implement - return user + type; + return "\n------------------\nuser: " + + user + + ",\ntype: " + + type + + ",\naccess-track: " + + accessLog + + ",\nvalue-track: " + + valueLog + + "\n------------------\n"; } } } diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java index 139c5e0a..31aeab48 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java @@ -17,6 +17,10 @@ interface AuditLog { void appendPropertyValue(String property, String value); + void initPropertyToAccessTrack(String property); + + void markAccessed(String property); + String toJson(); } @@ -28,5 +32,9 @@ interface AuditLog { void appendPropertyValue(String requestUuid, String property, String value); + void initPropertyToAccessTrack(String requestUuid, String property); + + void markAccessed(String requestUuid, String property); + void saveToFileAndRemove(String requestUuid); } From 764115bcdab1b85b15a94cf3b5059a5707c5bc9e Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 19 May 2026 11:50:58 +0200 Subject: [PATCH 04/44] LogContext: remove AUDIT context --- .../main/java/de/ii/xtraplatform/base/domain/LogContext.java | 3 +-- .../java/de/ii/xtraplatform/services/app/ServicesEndpoint.java | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/LogContext.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/LogContext.java index 03353a34..ecd9e7e6 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/LogContext.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/LogContext.java @@ -36,8 +36,7 @@ private LogContext() { public enum CONTEXT { SERVICE, - REQUEST, - AUDIT + REQUEST } public enum MARKER implements MyMarker { diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java index 67d96aa0..cf9b0d2b 100755 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java @@ -360,7 +360,6 @@ private void setupRequestLogging( String serviceId, Integer version, ContainerRequestContext containerRequestContext) { String uuid = LogContext.generateRandomUuid().toString(); containerRequestContext.setProperty("REQUEST_ID", uuid); - LogContext.put(LogContext.CONTEXT.AUDIT, uuid); if (LOGGER.isDebugEnabled() || LOGGER.isDebugEnabled(MARKER.REQUEST)) { LogContext.put(LogContext.CONTEXT.REQUEST, uuid); From b9a31ef0f42f4f54f499dff6688877707b304b61 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Wed, 20 May 2026 12:16:31 +0200 Subject: [PATCH 05/44] expand AuditLogger --- .../base/app/AuditLoggerImpl.java | 115 +++++++++++------- .../xtraplatform/base/domain/AuditLogger.java | 34 +++--- .../services/app/ServicesEndpoint.java | 20 ++- 3 files changed, 108 insertions(+), 61 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java index 9a5a0c4e..409e4d27 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java @@ -11,9 +11,11 @@ import de.ii.xtraplatform.base.domain.AuditLogger; import jakarta.inject.Inject; import jakarta.inject.Singleton; +import java.time.Instant; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; @@ -29,102 +31,127 @@ public class AuditLoggerImpl implements AuditLogger { @Inject AuditLoggerImpl() {} - private AuditLog lazyInitOrGetAuditLog(String requestUuid) { - return auditLogMapping.computeIfAbsent(requestUuid, k -> new AuditLogImpl()); + private AuditLog lazyInitOrGetAuditLog(String requestId) { + return auditLogMapping.computeIfAbsent(requestId, k -> new AuditLogImpl(requestId)); } @Override - public void initUser(String requestUuid, String user) { - lazyInitOrGetAuditLog(requestUuid).initUser(user); + public void initApi(String requestId, String api) { + lazyInitOrGetAuditLog(requestId).initApi(api); } @Override - public void initType(String requestUuid, String type) { - lazyInitOrGetAuditLog(requestUuid).initType(type); + public void initActor(String requestId, String actorType, String actorId) { + lazyInitOrGetAuditLog(requestId).initActor(actorType, actorId); } @Override - public void initPropertyToValueTrack(String requestUuid, String property) { - lazyInitOrGetAuditLog(requestUuid).initPropertyToValueTrack(property); + public void initPropertyToValueTrack(String requestId, String type, String property) { + lazyInitOrGetAuditLog(requestId).initPropertyToValueTrack(type, property); } + // ToDo: Evaluate if warnig is justified + @SuppressWarnings("PMD.UseObjectForClearerAPI") @Override - public void appendPropertyValue(String requestUuid, String property, String value) { - lazyInitOrGetAuditLog(requestUuid).appendPropertyValue(property, value); + public void appendPropertyValue(String requestId, String type, String property, String value) { + lazyInitOrGetAuditLog(requestId).appendPropertyValue(type, property, value); } @Override - public void initPropertyToAccessTrack(String requestUuid, String property) { - lazyInitOrGetAuditLog(requestUuid).initPropertyToAccessTrack(property); + public void initPropertyToAccessTrack(String requestId, String type, String property) { + lazyInitOrGetAuditLog(requestId).initPropertyToAccessTrack(type, property); } @Override - public void markAccessed(String requestUuid, String property) { - lazyInitOrGetAuditLog(requestUuid).markAccessed(property); + public void markAccessed(String requestId, String type, String property) { + lazyInitOrGetAuditLog(requestId).markAccessed(type, property); } @Override - public void saveToFileAndRemove(String requestUuid) { + public void saveToFileAndRemove(String requestId) { // ToDo Implement - if (LOGGER.isInfoEnabled()) { - LOGGER.info(lazyInitOrGetAuditLog(requestUuid).toJson()); + if (auditLogMapping.containsKey(requestId)) { + if (LOGGER.isInfoEnabled()) { + LOGGER.info(lazyInitOrGetAuditLog(requestId).toJson()); + } + auditLogMapping.remove(requestId); + } else { + if (LOGGER.isErrorEnabled()) { + LOGGER.error("No AuditLog-object found for requestId " + requestId); + } } } private static class AuditLogImpl implements AuditLogger.AuditLog { - private String user; - private String type; - private final Map> valueLog = new LinkedHashMap<>(); - private final Map accessLog = new LinkedHashMap<>(); + private final String id; + private final Instant started; + private String api; + private final Map actor = new LinkedHashMap<>(); - AuditLogImpl() {} + private final Map>> valueLog = new LinkedHashMap<>(); + private final Map>> accessLog = new LinkedHashMap<>(); + + AuditLogImpl(String id) { + this.id = id; + this.started = Instant.now(); + } @Override - public void initUser(String user) { - // ToDo Safety checks - this.user = user; + public void initApi(String api) { + if (Objects.isNull(this.api)) { + this.api = api; + } } @Override - public void initType(String type) { - // ToDo Safety checks - this.type = type; + public void initActor(String actorType, String actorId) { + actor.computeIfAbsent("type", k -> actorType); + actor.computeIfAbsent("id", k -> actorId); } @Override - public void initPropertyToValueTrack(String property) { - valueLog.computeIfAbsent(property, p -> new LinkedHashSet<>()); + public void initPropertyToValueTrack(String type, String property) { + valueLog.computeIfAbsent(type, k -> new LinkedHashMap<>()); + valueLog.get(type).computeIfAbsent(property, k -> new LinkedHashSet<>()); } @Override - public void appendPropertyValue(String property, String value) { - if (valueLog.containsKey(property)) { - valueLog.get(property).add(value); + public void appendPropertyValue(String type, String property, String value) { + if (valueLog.containsKey(type) && valueLog.get(type).containsKey(property)) { + valueLog.get(type).get(property).add(value); } } @Override - public void initPropertyToAccessTrack(String property) { - accessLog.computeIfAbsent(property, p -> false); + public void initPropertyToAccessTrack(String type, String property) { + accessLog.computeIfAbsent(type, k -> new LinkedHashMap<>()); + accessLog.get(type).computeIfAbsent(property, k -> new LinkedHashSet<>()); } @Override - public void markAccessed(String property) { - if (accessLog.containsKey(property)) { - accessLog.put(property, true); + public void markAccessed(String type, String property) { + if (accessLog.containsKey(type) && accessLog.get(type).containsKey(property)) { + accessLog.get(type).get(property).add(true); } } @Override public String toJson() { + Instant finished = Instant.now(); // ToDo implement - return "\n------------------\nuser: " - + user - + ",\ntype: " - + type - + ",\naccess-track: " + return "\n------------------\nid: " + + id + + "\nstarted: " + + started + + "\nfinished: " + + finished + + "\napi: " + + api + + "\nactor: " + + actor + + "\naccess-track: " + accessLog - + ",\nvalue-track: " + + "\nvalue-track: " + valueLog + "\n------------------\n"; } diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java index 31aeab48..98d70a41 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java @@ -8,33 +8,35 @@ package de.ii.xtraplatform.base.domain; public interface AuditLogger { - interface AuditLog { - void initUser(String user); + void initApi(String requestId, String api); - void initType(String type); + void initActor(String requestId, String actorType, String actorId); - void initPropertyToValueTrack(String property); + void initPropertyToValueTrack(String requestId, String type, String property); - void appendPropertyValue(String property, String value); + // ToDo: Evaluate if warnig is justified + @SuppressWarnings("PMD.UseObjectForClearerAPI") + void appendPropertyValue(String requestId, String type, String property, String value); - void initPropertyToAccessTrack(String property); + void initPropertyToAccessTrack(String requestId, String type, String property); - void markAccessed(String property); + void markAccessed(String requestId, String type, String property); - String toJson(); - } + void saveToFileAndRemove(String requestId); - void initUser(String requestUuid, String user); + interface AuditLog { + void initApi(String api); - void initType(String requestUuid, String type); + void initActor(String actorType, String actorId); - void initPropertyToValueTrack(String requestUuid, String property); + void initPropertyToValueTrack(String type, String property); - void appendPropertyValue(String requestUuid, String property, String value); + void appendPropertyValue(String type, String property, String value); - void initPropertyToAccessTrack(String requestUuid, String property); + void initPropertyToAccessTrack(String type, String property); - void markAccessed(String requestUuid, String property); + void markAccessed(String type, String property); - void saveToFileAndRemove(String requestUuid); + String toJson(); + } } diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java index cf9b0d2b..cd355f83 100755 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java @@ -10,6 +10,7 @@ import com.github.azahnen.dagger.annotations.AutoBind; import com.google.common.base.Joiner; import dagger.Lazy; +import de.ii.xtraplatform.base.domain.AuditLogger; import de.ii.xtraplatform.base.domain.LogContext; import de.ii.xtraplatform.base.domain.LogContext.MARKER; import de.ii.xtraplatform.entities.domain.EntityRegistry; @@ -79,6 +80,8 @@ public class ServicesEndpoint implements Endpoint { private final Lazy> serviceResources; private final Lazy> serviceListingProviders; + private final AuditLogger auditLogger; + @Inject public ServicesEndpoint( EntityRegistry entityRegistry, @@ -87,7 +90,8 @@ public ServicesEndpoint( StaticResourceHandler staticResourceHandler, Lazy> loginHandler, Lazy> serviceResources, - Lazy> serviceListingProviders) { + Lazy> serviceListingProviders, + AuditLogger auditLogger) { this.entityRegistry = entityRegistry; this.servicesContext = servicesContext; this.serviceContext = serviceContext; @@ -95,6 +99,7 @@ public ServicesEndpoint( this.loginHandler = loginHandler; this.serviceResources = serviceResources; this.serviceListingProviders = serviceListingProviders; + this.auditLogger = auditLogger; } @GET @@ -360,6 +365,19 @@ private void setupRequestLogging( String serviceId, Integer version, ContainerRequestContext containerRequestContext) { String uuid = LogContext.generateRandomUuid().toString(); containerRequestContext.setProperty("REQUEST_ID", uuid); + + // ToDo: To avoid memory leak: Either check if this request should be logged or pass the + // necessary information down to FeatureStream + if (Objects.nonNull(serviceId)) { + auditLogger.initApi(uuid, serviceId); + } + Principal principal = containerRequestContext.getSecurityContext().getUserPrincipal(); + if (Objects.nonNull(principal)) { + // ToDo: Find a way to get userType (cant cast to User because of circular dependency, also + // find a way to set the user + auditLogger.initActor(uuid, "MISSING", principal.getName()); + } + if (LOGGER.isDebugEnabled() || LOGGER.isDebugEnabled(MARKER.REQUEST)) { LogContext.put(LogContext.CONTEXT.REQUEST, uuid); From ea3f61c9dce0abf644d476e53d50809839a78aff Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Wed, 20 May 2026 12:29:01 +0200 Subject: [PATCH 06/44] small refactor --- .../java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java | 6 +++--- .../java/de/ii/xtraplatform/base/domain/AuditLogger.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java index 409e4d27..66e1c949 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java @@ -63,8 +63,8 @@ public void initPropertyToAccessTrack(String requestId, String type, String prop } @Override - public void markAccessed(String requestId, String type, String property) { - lazyInitOrGetAuditLog(requestId).markAccessed(type, property); + public void markPropertyAccessed(String requestId, String type, String property) { + lazyInitOrGetAuditLog(requestId).markPropertyAccessed(type, property); } @Override @@ -129,7 +129,7 @@ public void initPropertyToAccessTrack(String type, String property) { } @Override - public void markAccessed(String type, String property) { + public void markPropertyAccessed(String type, String property) { if (accessLog.containsKey(type) && accessLog.get(type).containsKey(property)) { accessLog.get(type).get(property).add(true); } diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java index 98d70a41..5f19dc6a 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java @@ -20,7 +20,7 @@ public interface AuditLogger { void initPropertyToAccessTrack(String requestId, String type, String property); - void markAccessed(String requestId, String type, String property); + void markPropertyAccessed(String requestId, String type, String property); void saveToFileAndRemove(String requestId); @@ -35,7 +35,7 @@ interface AuditLog { void initPropertyToAccessTrack(String type, String property); - void markAccessed(String type, String property); + void markPropertyAccessed(String type, String property); String toJson(); } From b00bba266e2d37837d51067042a3f7f25af55571 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Wed, 20 May 2026 14:48:02 +0200 Subject: [PATCH 07/44] AuditLogger: operation handling --- .../base/app/AuditLoggerImpl.java | 90 ++++++++++++++++--- .../xtraplatform/base/domain/AuditLogger.java | 18 ++++ .../services/app/ServicesEndpoint.java | 17 +++- 3 files changed, 114 insertions(+), 11 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java index 66e1c949..a127dd01 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java @@ -11,12 +11,14 @@ import de.ii.xtraplatform.base.domain.AuditLogger; import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.ws.rs.core.MultivaluedMap; import java.time.Instant; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; import java.util.Set; +// ToDo Evaluate if ConcurrentHashMap is actually needed. Current analysis: Threads could access +// the auditLogMapping at the same time. But there is no scenario in which two threads will access +// the same auditLog object. So apart from the auditLogMapping, no ConcurrentHashMap is needed. import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +47,26 @@ public void initActor(String requestId, String actorType, String actorId) { lazyInitOrGetAuditLog(requestId).initActor(actorType, actorId); } + @Override + public void initOperationMethod(String requestId, String method) { + lazyInitOrGetAuditLog(requestId).initOperationMethod(method); + } + + @Override + public void initOperationPath(String requestId, String path) { + lazyInitOrGetAuditLog(requestId).initOperationPath(path); + } + + @Override + public void initOperationHeaders(String requestId, MultivaluedMap headers) { + lazyInitOrGetAuditLog(requestId).initOperationHeaders(headers); + } + + @Override + public void initOperationStatus(String requestId, String status) { + lazyInitOrGetAuditLog(requestId).initOperationStatus(status); + } + @Override public void initPropertyToValueTrack(String requestId, String type, String property) { lazyInitOrGetAuditLog(requestId).initPropertyToValueTrack(type, property); @@ -77,7 +99,7 @@ public void saveToFileAndRemove(String requestId) { auditLogMapping.remove(requestId); } else { if (LOGGER.isErrorEnabled()) { - LOGGER.error("No AuditLog-object found for requestId " + requestId); + LOGGER.error("No AuditLog-object found for requestId {}", requestId); } } } @@ -86,14 +108,16 @@ private static class AuditLogImpl implements AuditLogger.AuditLog { private final String id; private final Instant started; private String api; - private final Map actor = new LinkedHashMap<>(); + private final Map actor = new ConcurrentHashMap<>(); + private final Operation operation; - private final Map>> valueLog = new LinkedHashMap<>(); - private final Map>> accessLog = new LinkedHashMap<>(); + private final Map>> valueLog = new ConcurrentHashMap<>(); + private final Map>> accessLog = new ConcurrentHashMap<>(); AuditLogImpl(String id) { this.id = id; this.started = Instant.now(); + this.operation = new Operation(); } @Override @@ -109,10 +133,30 @@ public void initActor(String actorType, String actorId) { actor.computeIfAbsent("id", k -> actorId); } + @Override + public void initOperationMethod(String method) { + operation.method = method; + } + + @Override + public void initOperationPath(String path) { + operation.path = path; + } + + @Override + public void initOperationHeaders(MultivaluedMap headers) { + operation.headers = headers; + } + + @Override + public void initOperationStatus(String status) { + operation.status = status; + } + @Override public void initPropertyToValueTrack(String type, String property) { - valueLog.computeIfAbsent(type, k -> new LinkedHashMap<>()); - valueLog.get(type).computeIfAbsent(property, k -> new LinkedHashSet<>()); + valueLog.computeIfAbsent(type, k -> new ConcurrentHashMap<>()); + valueLog.get(type).computeIfAbsent(property, k -> ConcurrentHashMap.newKeySet()); } @Override @@ -124,8 +168,8 @@ public void appendPropertyValue(String type, String property, String value) { @Override public void initPropertyToAccessTrack(String type, String property) { - accessLog.computeIfAbsent(type, k -> new LinkedHashMap<>()); - accessLog.get(type).computeIfAbsent(property, k -> new LinkedHashSet<>()); + accessLog.computeIfAbsent(type, k -> new ConcurrentHashMap<>()); + accessLog.get(type).computeIfAbsent(property, k -> ConcurrentHashMap.newKeySet()); } @Override @@ -149,11 +193,37 @@ public String toJson() { + api + "\nactor: " + actor + + "\noperation: " + + operation + "\naccess-track: " + accessLog + "\nvalue-track: " + valueLog + "\n------------------\n"; } + + private static final class Operation { + private String method; + private String path; + private MultivaluedMap headers; + private String status; + + @Override + public String toString() { + return "Operation{" + + "method='" + + method + + '\'' + + ", path='" + + path + + '\'' + + ", headers=" + + headers + + ", status='" + + status + + '\'' + + '}'; + } + } } } diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java index 5f19dc6a..d739215f 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java @@ -7,6 +7,8 @@ */ package de.ii.xtraplatform.base.domain; +import jakarta.ws.rs.core.MultivaluedMap; + public interface AuditLogger { void initApi(String requestId, String api); @@ -14,6 +16,14 @@ public interface AuditLogger { void initPropertyToValueTrack(String requestId, String type, String property); + void initOperationMethod(String requestId, String method); + + void initOperationPath(String requestId, String path); + + void initOperationHeaders(String requestId, MultivaluedMap headers); + + void initOperationStatus(String requestId, String status); + // ToDo: Evaluate if warnig is justified @SuppressWarnings("PMD.UseObjectForClearerAPI") void appendPropertyValue(String requestId, String type, String property, String value); @@ -29,6 +39,14 @@ interface AuditLog { void initActor(String actorType, String actorId); + void initOperationMethod(String method); + + void initOperationPath(String path); + + void initOperationHeaders(MultivaluedMap headers); + + void initOperationStatus(String status); + void initPropertyToValueTrack(String type, String property); void appendPropertyValue(String type, String property, String value); diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java index cd355f83..af8d115c 100755 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java @@ -42,6 +42,7 @@ import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; import java.io.IOException; @@ -374,9 +375,23 @@ private void setupRequestLogging( Principal principal = containerRequestContext.getSecurityContext().getUserPrincipal(); if (Objects.nonNull(principal)) { // ToDo: Find a way to get userType (cant cast to User because of circular dependency, also - // find a way to set the user + // for testing purposes find a way to set the user auditLogger.initActor(uuid, "MISSING", principal.getName()); } + String method = containerRequestContext.getMethod(); + if (Objects.nonNull(method)) { + auditLogger.initOperationMethod(uuid, method); + } + String path = containerRequestContext.getUriInfo().getPath(); + if (Objects.nonNull(path)) { + auditLogger.initOperationPath(uuid, path); + } + // ToDo Check if headers should be set + MultivaluedMap headers = containerRequestContext.getHeaders(); + if (Objects.nonNull(headers)) { + auditLogger.initOperationHeaders(uuid, headers); + } + // ToDo: Find a way to get status if (LOGGER.isDebugEnabled() || LOGGER.isDebugEnabled(MARKER.REQUEST)) { LogContext.put(LogContext.CONTEXT.REQUEST, uuid); From 6c7454c665aca1f2b7c6cf56413bf2cf19a3088f Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Thu, 21 May 2026 13:48:19 +0200 Subject: [PATCH 08/44] Audit service: work on JSON output --- .../base/app/AuditLoggerImpl.java | 113 ++++++++++++++---- .../xtraplatform/base/domain/AuditLogger.java | 6 +- 2 files changed, 92 insertions(+), 27 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java index a127dd01..605256c6 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java @@ -7,12 +7,18 @@ */ package de.ii.xtraplatform.base.app; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.azahnen.dagger.annotations.AutoBind; import de.ii.xtraplatform.base.domain.AuditLogger; +import de.ii.xtraplatform.base.domain.Jackson; import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.ws.rs.core.MultivaluedMap; import java.time.Instant; +import java.util.Collection; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -26,12 +32,15 @@ @Singleton @AutoBind public class AuditLoggerImpl implements AuditLogger { + private final ObjectMapper objectMapper; private final Map auditLogMapping = new ConcurrentHashMap<>(); private static final Logger LOGGER = LoggerFactory.getLogger(AuditLoggerImpl.class); @Inject - AuditLoggerImpl() {} + AuditLoggerImpl(Jackson jackson) { + this.objectMapper = jackson.getDefaultObjectMapper(); + } private AuditLog lazyInitOrGetAuditLog(String requestId) { return auditLogMapping.computeIfAbsent(requestId, k -> new AuditLogImpl(requestId)); @@ -72,7 +81,7 @@ public void initPropertyToValueTrack(String requestId, String type, String prope lazyInitOrGetAuditLog(requestId).initPropertyToValueTrack(type, property); } - // ToDo: Evaluate if warnig is justified + // ToDo: Evaluate if warning is justified @SuppressWarnings("PMD.UseObjectForClearerAPI") @Override public void appendPropertyValue(String requestId, String type, String property, String value) { @@ -90,11 +99,11 @@ public void markPropertyAccessed(String requestId, String type, String property) } @Override - public void saveToFileAndRemove(String requestId) { + public void saveToFileAndRemove(String requestId) throws JsonProcessingException { // ToDo Implement if (auditLogMapping.containsKey(requestId)) { if (LOGGER.isInfoEnabled()) { - LOGGER.info(lazyInitOrGetAuditLog(requestId).toJson()); + LOGGER.info(lazyInitOrGetAuditLog(requestId).toJson(objectMapper.createObjectNode())); } auditLogMapping.remove(requestId); } else { @@ -104,7 +113,7 @@ public void saveToFileAndRemove(String requestId) { } } - private static class AuditLogImpl implements AuditLogger.AuditLog { + private static class AuditLogImpl implements AuditLog { private final String id; private final Instant started; private String api; @@ -180,26 +189,80 @@ public void markPropertyAccessed(String type, String property) { } @Override - public String toJson() { - Instant finished = Instant.now(); - // ToDo implement - return "\n------------------\nid: " - + id - + "\nstarted: " - + started - + "\nfinished: " - + finished - + "\napi: " - + api - + "\nactor: " - + actor - + "\noperation: " - + operation - + "\naccess-track: " - + accessLog - + "\nvalue-track: " - + valueLog - + "\n------------------\n"; + public String toJson(ObjectNode root) { + root.put("id", id); + root.put("started", started.toString()); + // ToDo Evaluate if it is okay to measure the finish time this early + root.put("finished", Instant.now().toString()); + + ObjectNode actorNode = root.putObject("actor"); + putWithCheck(actorNode, "type", actor.get("type")); + putWithCheck(actorNode, "id", actor.get("id")); + + ObjectNode operationNode = root.putObject("operation"); + putWithCheck(operationNode, "method", operation.method); + putWithCheck(operationNode, "path", operation.path); + putMultivaluedMap(operationNode, "headers", operation.headers); + putWithCheck(operationNode, "status", operation.status); + + ObjectNode targetNode = root.putObject("target"); + putTarget(targetNode, valueLog, accessLog); + + return root + "\n" + valueLog + "\n" + accessLog; + } + + private void putWithCheck(ObjectNode root, String key, Object value) { + if (Objects.nonNull(value)) { + root.put(key, value.toString()); + } else { + root.putNull(key); + } + } + + private void putCollection(ObjectNode root, String key, Collection collection) { + if (Objects.isNull(collection)) { + root.putNull(key); + return; + } + + ArrayNode arrayNode = root.putArray(key); + for (Object item : collection) { + arrayNode.addPOJO(item); + } + } + + private void putMultivaluedMap( + ObjectNode root, String key, MultivaluedMap multiMap) { + if (Objects.isNull(multiMap)) { + root.putNull(key); + return; + } + + ObjectNode mapNode = root.putObject(key); + for (String mapKey : multiMap.keySet()) { + if (multiMap.get(mapKey).isEmpty()) { + mapNode.putNull(mapKey); + continue; + } + + if (multiMap.get(mapKey).size() > 1) { + putCollection(mapNode, mapKey, multiMap.get(mapKey)); + continue; + } + + mapNode.put(mapKey, multiMap.getFirst(mapKey)); + } + } + + private void putTarget( + ObjectNode root, + Map>> valueLog, + Map>> accessLog) { + // ToDo finish + ObjectNode typesNode = root.putObject("typesNode"); + for (String type : valueLog.keySet()) { + continue; + } } private static final class Operation { diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java index d739215f..4fcef11f 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java @@ -7,6 +7,8 @@ */ package de.ii.xtraplatform.base.domain; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.ws.rs.core.MultivaluedMap; public interface AuditLogger { @@ -32,7 +34,7 @@ public interface AuditLogger { void markPropertyAccessed(String requestId, String type, String property); - void saveToFileAndRemove(String requestId); + void saveToFileAndRemove(String requestId) throws JsonProcessingException; interface AuditLog { void initApi(String api); @@ -55,6 +57,6 @@ interface AuditLog { void markPropertyAccessed(String type, String property); - String toJson(); + String toJson(ObjectNode root); } } From cabd43416c890c4a313ea8183fde02e9ce3ca7b6 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Thu, 21 May 2026 16:26:29 +0200 Subject: [PATCH 09/44] refactor --- ...AuditLoggerImpl.java => AuditLogImpl.java} | 47 +++++++------------ .../{AuditLogger.java => AuditLog.java} | 8 ++-- .../services/app/ServicesEndpoint.java | 18 +++---- 3 files changed, 30 insertions(+), 43 deletions(-) rename xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/{AuditLoggerImpl.java => AuditLogImpl.java} (88%) rename xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/{AuditLogger.java => AuditLog.java} (91%) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java similarity index 88% rename from xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java rename to xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java index 605256c6..c2c33415 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLoggerImpl.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java @@ -12,7 +12,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.azahnen.dagger.annotations.AutoBind; -import de.ii.xtraplatform.base.domain.AuditLogger; +import de.ii.xtraplatform.base.domain.AuditLog; import de.ii.xtraplatform.base.domain.Jackson; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -31,19 +31,19 @@ @Singleton @AutoBind -public class AuditLoggerImpl implements AuditLogger { +public class AuditLogImpl implements AuditLog { private final ObjectMapper objectMapper; - private final Map auditLogMapping = new ConcurrentHashMap<>(); + private final Map auditLogMapping = new ConcurrentHashMap<>(); - private static final Logger LOGGER = LoggerFactory.getLogger(AuditLoggerImpl.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogImpl.class); @Inject - AuditLoggerImpl(Jackson jackson) { + AuditLogImpl(Jackson jackson) { this.objectMapper = jackson.getDefaultObjectMapper(); } - private AuditLog lazyInitOrGetAuditLog(String requestId) { - return auditLogMapping.computeIfAbsent(requestId, k -> new AuditLogImpl(requestId)); + private Log lazyInitOrGetAuditLog(String requestId) { + return auditLogMapping.computeIfAbsent(requestId, k -> new LogImpl(requestId)); } @Override @@ -99,11 +99,14 @@ public void markPropertyAccessed(String requestId, String type, String property) } @Override - public void saveToFileAndRemove(String requestId) throws JsonProcessingException { + public void saveLogToFileAndRemove(String requestId) throws JsonProcessingException { // ToDo Implement if (auditLogMapping.containsKey(requestId)) { if (LOGGER.isInfoEnabled()) { - LOGGER.info(lazyInitOrGetAuditLog(requestId).toJson(objectMapper.createObjectNode())); + LOGGER.info( + lazyInitOrGetAuditLog(requestId) + .toObjectNode(objectMapper.createObjectNode()) + .toString()); } auditLogMapping.remove(requestId); } else { @@ -113,7 +116,7 @@ public void saveToFileAndRemove(String requestId) throws JsonProcessingException } } - private static class AuditLogImpl implements AuditLog { + private static class LogImpl implements Log { private final String id; private final Instant started; private String api; @@ -123,7 +126,7 @@ private static class AuditLogImpl implements AuditLog { private final Map>> valueLog = new ConcurrentHashMap<>(); private final Map>> accessLog = new ConcurrentHashMap<>(); - AuditLogImpl(String id) { + LogImpl(String id) { this.id = id; this.started = Instant.now(); this.operation = new Operation(); @@ -189,7 +192,7 @@ public void markPropertyAccessed(String type, String property) { } @Override - public String toJson(ObjectNode root) { + public ObjectNode toObjectNode(ObjectNode root) { root.put("id", id); root.put("started", started.toString()); // ToDo Evaluate if it is okay to measure the finish time this early @@ -208,7 +211,7 @@ public String toJson(ObjectNode root) { ObjectNode targetNode = root.putObject("target"); putTarget(targetNode, valueLog, accessLog); - return root + "\n" + valueLog + "\n" + accessLog; + return root; } private void putWithCheck(ObjectNode root, String key, Object value) { @@ -254,6 +257,7 @@ private void putMultivaluedMap( } } + @SuppressWarnings("PMD") private void putTarget( ObjectNode root, Map>> valueLog, @@ -270,23 +274,6 @@ private static final class Operation { private String path; private MultivaluedMap headers; private String status; - - @Override - public String toString() { - return "Operation{" - + "method='" - + method - + '\'' - + ", path='" - + path - + '\'' - + ", headers=" - + headers - + ", status='" - + status - + '\'' - + '}'; - } } } } diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLog.java similarity index 91% rename from xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java rename to xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLog.java index 4fcef11f..83d3e4e7 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogger.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLog.java @@ -11,7 +11,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.ws.rs.core.MultivaluedMap; -public interface AuditLogger { +public interface AuditLog { void initApi(String requestId, String api); void initActor(String requestId, String actorType, String actorId); @@ -34,9 +34,9 @@ public interface AuditLogger { void markPropertyAccessed(String requestId, String type, String property); - void saveToFileAndRemove(String requestId) throws JsonProcessingException; + void saveLogToFileAndRemove(String requestId) throws JsonProcessingException; - interface AuditLog { + interface Log { void initApi(String api); void initActor(String actorType, String actorId); @@ -57,6 +57,6 @@ interface AuditLog { void markPropertyAccessed(String type, String property); - String toJson(ObjectNode root); + ObjectNode toObjectNode(ObjectNode root); } } diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java index af8d115c..a4fbd803 100755 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java @@ -10,7 +10,7 @@ import com.github.azahnen.dagger.annotations.AutoBind; import com.google.common.base.Joiner; import dagger.Lazy; -import de.ii.xtraplatform.base.domain.AuditLogger; +import de.ii.xtraplatform.base.domain.AuditLog; import de.ii.xtraplatform.base.domain.LogContext; import de.ii.xtraplatform.base.domain.LogContext.MARKER; import de.ii.xtraplatform.entities.domain.EntityRegistry; @@ -81,7 +81,7 @@ public class ServicesEndpoint implements Endpoint { private final Lazy> serviceResources; private final Lazy> serviceListingProviders; - private final AuditLogger auditLogger; + private final AuditLog auditLog; @Inject public ServicesEndpoint( @@ -92,7 +92,7 @@ public ServicesEndpoint( Lazy> loginHandler, Lazy> serviceResources, Lazy> serviceListingProviders, - AuditLogger auditLogger) { + AuditLog auditLog) { this.entityRegistry = entityRegistry; this.servicesContext = servicesContext; this.serviceContext = serviceContext; @@ -100,7 +100,7 @@ public ServicesEndpoint( this.loginHandler = loginHandler; this.serviceResources = serviceResources; this.serviceListingProviders = serviceListingProviders; - this.auditLogger = auditLogger; + this.auditLog = auditLog; } @GET @@ -370,26 +370,26 @@ private void setupRequestLogging( // ToDo: To avoid memory leak: Either check if this request should be logged or pass the // necessary information down to FeatureStream if (Objects.nonNull(serviceId)) { - auditLogger.initApi(uuid, serviceId); + auditLog.initApi(uuid, serviceId); } Principal principal = containerRequestContext.getSecurityContext().getUserPrincipal(); if (Objects.nonNull(principal)) { // ToDo: Find a way to get userType (cant cast to User because of circular dependency, also // for testing purposes find a way to set the user - auditLogger.initActor(uuid, "MISSING", principal.getName()); + auditLog.initActor(uuid, "MISSING", principal.getName()); } String method = containerRequestContext.getMethod(); if (Objects.nonNull(method)) { - auditLogger.initOperationMethod(uuid, method); + auditLog.initOperationMethod(uuid, method); } String path = containerRequestContext.getUriInfo().getPath(); if (Objects.nonNull(path)) { - auditLogger.initOperationPath(uuid, path); + auditLog.initOperationPath(uuid, path); } // ToDo Check if headers should be set MultivaluedMap headers = containerRequestContext.getHeaders(); if (Objects.nonNull(headers)) { - auditLogger.initOperationHeaders(uuid, headers); + auditLog.initOperationHeaders(uuid, headers); } // ToDo: Find a way to get status From 2f5b9f97587011101b5d6e873409f14fdbc2c3da Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Fri, 22 May 2026 13:49:27 +0200 Subject: [PATCH 10/44] AuditLog: target-logging and ObjectMapper support --- .../xtraplatform/base/app/AuditLogImpl.java | 203 +++++------------- .../ii/xtraplatform/base/domain/AuditLog.java | 22 +- 2 files changed, 59 insertions(+), 166 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java index c2c33415..3e16802e 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java @@ -7,10 +7,10 @@ */ package de.ii.xtraplatform.base.app; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.azahnen.dagger.annotations.AutoBind; import de.ii.xtraplatform.base.domain.AuditLog; import de.ii.xtraplatform.base.domain.Jackson; @@ -18,10 +18,8 @@ import jakarta.inject.Singleton; import jakarta.ws.rs.core.MultivaluedMap; import java.time.Instant; -import java.util.Collection; import java.util.Map; import java.util.Objects; -import java.util.Set; // ToDo Evaluate if ConcurrentHashMap is actually needed. Current analysis: Threads could access // the auditLogMapping at the same time. But there is no scenario in which two threads will access // the same auditLog object. So apart from the auditLogMapping, no ConcurrentHashMap is needed. @@ -32,11 +30,10 @@ @Singleton @AutoBind public class AuditLogImpl implements AuditLog { + private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogImpl.class); private final ObjectMapper objectMapper; private final Map auditLogMapping = new ConcurrentHashMap<>(); - private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogImpl.class); - @Inject AuditLogImpl(Jackson jackson) { this.objectMapper = jackson.getDefaultObjectMapper(); @@ -77,37 +74,17 @@ public void initOperationStatus(String requestId, String status) { } @Override - public void initPropertyToValueTrack(String requestId, String type, String property) { - lazyInitOrGetAuditLog(requestId).initPropertyToValueTrack(type, property); - } - - // ToDo: Evaluate if warning is justified - @SuppressWarnings("PMD.UseObjectForClearerAPI") - @Override - public void appendPropertyValue(String requestId, String type, String property, String value) { - lazyInitOrGetAuditLog(requestId).appendPropertyValue(type, property, value); - } - - @Override - public void initPropertyToAccessTrack(String requestId, String type, String property) { - lazyInitOrGetAuditLog(requestId).initPropertyToAccessTrack(type, property); - } - - @Override - public void markPropertyAccessed(String requestId, String type, String property) { - lazyInitOrGetAuditLog(requestId).markPropertyAccessed(type, property); + public void initTarget(String requestId, Map target) { + lazyInitOrGetAuditLog(requestId).initTarget(target); } @Override public void saveLogToFileAndRemove(String requestId) throws JsonProcessingException { - // ToDo Implement if (auditLogMapping.containsKey(requestId)) { - if (LOGGER.isInfoEnabled()) { - LOGGER.info( - lazyInitOrGetAuditLog(requestId) - .toObjectNode(objectMapper.createObjectNode()) - .toString()); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(objectMapper.writeValueAsString(auditLogMapping.get(requestId))); } + // ToDo save to file auditLogMapping.remove(requestId); } else { if (LOGGER.isErrorEnabled()) { @@ -116,164 +93,96 @@ public void saveLogToFileAndRemove(String requestId) throws JsonProcessingExcept } } - private static class LogImpl implements Log { + @JsonPropertyOrder({"id", "started", "finished", "api", "actor", "operation", "target"}) + public static class LogImpl implements Log { private final String id; private final Instant started; - private String api; private final Map actor = new ConcurrentHashMap<>(); - private final Operation operation; + private final Map operation = new ConcurrentHashMap<>(); - private final Map>> valueLog = new ConcurrentHashMap<>(); - private final Map>> accessLog = new ConcurrentHashMap<>(); + private Map target; + private Instant finished; + private String api; LogImpl(String id) { this.id = id; this.started = Instant.now(); - this.operation = new Operation(); } - @Override - public void initApi(String api) { - if (Objects.isNull(this.api)) { - this.api = api; - } + @JsonProperty("id") + public String getId() { + return id; } - @Override - public void initActor(String actorType, String actorId) { - actor.computeIfAbsent("type", k -> actorType); - actor.computeIfAbsent("id", k -> actorId); + @JsonProperty("started") + public String getStarted() { + return started.toString(); } - @Override - public void initOperationMethod(String method) { - operation.method = method; + // Hier bin ich mir unsicher, ob das so sinnvoll ist. + @JsonProperty("finished") + public String finish() { + return Instant.now().toString(); } - @Override - public void initOperationPath(String path) { - operation.path = path; + @JsonProperty("api") + public String getApi() { + return api; } - @Override - public void initOperationHeaders(MultivaluedMap headers) { - operation.headers = headers; + @JsonProperty("actor") + public Map getActor() { + return actor; } - @Override - public void initOperationStatus(String status) { - operation.status = status; + @JsonProperty("operation") + public Map getOperation() { + return operation; } - @Override - public void initPropertyToValueTrack(String type, String property) { - valueLog.computeIfAbsent(type, k -> new ConcurrentHashMap<>()); - valueLog.get(type).computeIfAbsent(property, k -> ConcurrentHashMap.newKeySet()); + @JsonProperty("target") + public Map getTarget() { + return target; } @Override - public void appendPropertyValue(String type, String property, String value) { - if (valueLog.containsKey(type) && valueLog.get(type).containsKey(property)) { - valueLog.get(type).get(property).add(value); + public void initApi(String api) { + if (Objects.isNull(this.api)) { + this.api = api; } } @Override - public void initPropertyToAccessTrack(String type, String property) { - accessLog.computeIfAbsent(type, k -> new ConcurrentHashMap<>()); - accessLog.get(type).computeIfAbsent(property, k -> ConcurrentHashMap.newKeySet()); + public void initActor(String actorType, String actorId) { + actor.computeIfAbsent("type", k -> actorType); + actor.computeIfAbsent("id", k -> actorId); } @Override - public void markPropertyAccessed(String type, String property) { - if (accessLog.containsKey(type) && accessLog.get(type).containsKey(property)) { - accessLog.get(type).get(property).add(true); - } + public void initOperationMethod(String method) { + operation.computeIfAbsent("method", k -> method); } @Override - public ObjectNode toObjectNode(ObjectNode root) { - root.put("id", id); - root.put("started", started.toString()); - // ToDo Evaluate if it is okay to measure the finish time this early - root.put("finished", Instant.now().toString()); - - ObjectNode actorNode = root.putObject("actor"); - putWithCheck(actorNode, "type", actor.get("type")); - putWithCheck(actorNode, "id", actor.get("id")); - - ObjectNode operationNode = root.putObject("operation"); - putWithCheck(operationNode, "method", operation.method); - putWithCheck(operationNode, "path", operation.path); - putMultivaluedMap(operationNode, "headers", operation.headers); - putWithCheck(operationNode, "status", operation.status); - - ObjectNode targetNode = root.putObject("target"); - putTarget(targetNode, valueLog, accessLog); - - return root; - } - - private void putWithCheck(ObjectNode root, String key, Object value) { - if (Objects.nonNull(value)) { - root.put(key, value.toString()); - } else { - root.putNull(key); - } + public void initOperationPath(String path) { + operation.computeIfAbsent("path", k -> path); } - private void putCollection(ObjectNode root, String key, Collection collection) { - if (Objects.isNull(collection)) { - root.putNull(key); - return; - } - - ArrayNode arrayNode = root.putArray(key); - for (Object item : collection) { - arrayNode.addPOJO(item); - } + @Override + public void initOperationHeaders(MultivaluedMap headers) { + operation.computeIfAbsent("headers", k -> headers); } - private void putMultivaluedMap( - ObjectNode root, String key, MultivaluedMap multiMap) { - if (Objects.isNull(multiMap)) { - root.putNull(key); - return; - } - - ObjectNode mapNode = root.putObject(key); - for (String mapKey : multiMap.keySet()) { - if (multiMap.get(mapKey).isEmpty()) { - mapNode.putNull(mapKey); - continue; - } - - if (multiMap.get(mapKey).size() > 1) { - putCollection(mapNode, mapKey, multiMap.get(mapKey)); - continue; - } - - mapNode.put(mapKey, multiMap.getFirst(mapKey)); - } + @Override + public void initOperationStatus(String status) { + operation.computeIfAbsent("status", k -> status); } - @SuppressWarnings("PMD") - private void putTarget( - ObjectNode root, - Map>> valueLog, - Map>> accessLog) { - // ToDo finish - ObjectNode typesNode = root.putObject("typesNode"); - for (String type : valueLog.keySet()) { - continue; + @Override + public void initTarget(Map target) { + if (Objects.isNull(this.target)) { + this.target = target; } } - - private static final class Operation { - private String method; - private String path; - private MultivaluedMap headers; - private String status; - } } } diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLog.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLog.java index 83d3e4e7..d9bb7789 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLog.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLog.java @@ -8,16 +8,14 @@ package de.ii.xtraplatform.base.domain; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.ws.rs.core.MultivaluedMap; +import java.util.Map; public interface AuditLog { void initApi(String requestId, String api); void initActor(String requestId, String actorType, String actorId); - void initPropertyToValueTrack(String requestId, String type, String property); - void initOperationMethod(String requestId, String method); void initOperationPath(String requestId, String path); @@ -26,13 +24,7 @@ public interface AuditLog { void initOperationStatus(String requestId, String status); - // ToDo: Evaluate if warnig is justified - @SuppressWarnings("PMD.UseObjectForClearerAPI") - void appendPropertyValue(String requestId, String type, String property, String value); - - void initPropertyToAccessTrack(String requestId, String type, String property); - - void markPropertyAccessed(String requestId, String type, String property); + void initTarget(String requestId, Map target); void saveLogToFileAndRemove(String requestId) throws JsonProcessingException; @@ -49,14 +41,6 @@ interface Log { void initOperationStatus(String status); - void initPropertyToValueTrack(String type, String property); - - void appendPropertyValue(String type, String property, String value); - - void initPropertyToAccessTrack(String type, String property); - - void markPropertyAccessed(String type, String property); - - ObjectNode toObjectNode(ObjectNode root); + void initTarget(Map target); } } From 5b5f39fc717c7d8f51e3724ee96126ea94041173 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Fri, 22 May 2026 15:04:55 +0200 Subject: [PATCH 11/44] AuditLog: convert MultivaluedMap-headers to Map for better serialization --- .../xtraplatform/base/app/AuditLogImpl.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java index 3e16802e..daaeaa5f 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java @@ -18,6 +18,7 @@ import jakarta.inject.Singleton; import jakarta.ws.rs.core.MultivaluedMap; import java.time.Instant; +import java.util.HashMap; import java.util.Map; import java.util.Objects; // ToDo Evaluate if ConcurrentHashMap is actually needed. Current analysis: Threads could access @@ -99,9 +100,7 @@ public static class LogImpl implements Log { private final Instant started; private final Map actor = new ConcurrentHashMap<>(); private final Map operation = new ConcurrentHashMap<>(); - private Map target; - private Instant finished; private String api; LogImpl(String id) { @@ -170,7 +169,22 @@ public void initOperationPath(String path) { @Override public void initOperationHeaders(MultivaluedMap headers) { - operation.computeIfAbsent("headers", k -> headers); + Map headersReduced = new HashMap<>(); + + for (String key : headers.keySet()) { + if (headers.get(key).isEmpty()) { + continue; + } + + if (headers.get(key).size() == 1) { + headersReduced.put(key, headers.get(key).get(0)); + continue; + } + + headersReduced.put(key, headers.get(key)); + } + + operation.computeIfAbsent("headers", k -> headersReduced); } @Override From ce758f18e5b9e83580e5b20755b236d412fbc208 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 26 May 2026 11:57:17 +0200 Subject: [PATCH 12/44] keep property order by using LinkedHashMap --- .../java/de/ii/xtraplatform/base/app/AuditLogImpl.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java index daaeaa5f..3e4b6cb7 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java @@ -19,11 +19,9 @@ import jakarta.ws.rs.core.MultivaluedMap; import java.time.Instant; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; -// ToDo Evaluate if ConcurrentHashMap is actually needed. Current analysis: Threads could access -// the auditLogMapping at the same time. But there is no scenario in which two threads will access -// the same auditLog object. So apart from the auditLogMapping, no ConcurrentHashMap is needed. import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -98,8 +96,8 @@ public void saveLogToFileAndRemove(String requestId) throws JsonProcessingExcept public static class LogImpl implements Log { private final String id; private final Instant started; - private final Map actor = new ConcurrentHashMap<>(); - private final Map operation = new ConcurrentHashMap<>(); + private final Map actor = new LinkedHashMap<>(); + private final Map operation = new LinkedHashMap<>(); private Map target; private String api; @@ -118,7 +116,7 @@ public String getStarted() { return started.toString(); } - // Hier bin ich mir unsicher, ob das so sinnvoll ist. + // This works but im still analysing if it could result in undesired behavior @JsonProperty("finished") public String finish() { return Instant.now().toString(); From 46b875d92e03e15485ebe457a2dd36e1a29b02f3 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 26 May 2026 13:07:12 +0200 Subject: [PATCH 13/44] move audit-init to ApiRequestDispatcher --- .../services/app/ServicesEndpoint.java | 34 +------------------ 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java index a4fbd803..3cb6b4d5 100755 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java @@ -10,7 +10,6 @@ import com.github.azahnen.dagger.annotations.AutoBind; import com.google.common.base.Joiner; import dagger.Lazy; -import de.ii.xtraplatform.base.domain.AuditLog; import de.ii.xtraplatform.base.domain.LogContext; import de.ii.xtraplatform.base.domain.LogContext.MARKER; import de.ii.xtraplatform.entities.domain.EntityRegistry; @@ -42,7 +41,6 @@ import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; import java.io.IOException; @@ -81,8 +79,6 @@ public class ServicesEndpoint implements Endpoint { private final Lazy> serviceResources; private final Lazy> serviceListingProviders; - private final AuditLog auditLog; - @Inject public ServicesEndpoint( EntityRegistry entityRegistry, @@ -91,8 +87,7 @@ public ServicesEndpoint( StaticResourceHandler staticResourceHandler, Lazy> loginHandler, Lazy> serviceResources, - Lazy> serviceListingProviders, - AuditLog auditLog) { + Lazy> serviceListingProviders) { this.entityRegistry = entityRegistry; this.servicesContext = servicesContext; this.serviceContext = serviceContext; @@ -100,7 +95,6 @@ public ServicesEndpoint( this.loginHandler = loginHandler; this.serviceResources = serviceResources; this.serviceListingProviders = serviceListingProviders; - this.auditLog = auditLog; } @GET @@ -367,32 +361,6 @@ private void setupRequestLogging( String uuid = LogContext.generateRandomUuid().toString(); containerRequestContext.setProperty("REQUEST_ID", uuid); - // ToDo: To avoid memory leak: Either check if this request should be logged or pass the - // necessary information down to FeatureStream - if (Objects.nonNull(serviceId)) { - auditLog.initApi(uuid, serviceId); - } - Principal principal = containerRequestContext.getSecurityContext().getUserPrincipal(); - if (Objects.nonNull(principal)) { - // ToDo: Find a way to get userType (cant cast to User because of circular dependency, also - // for testing purposes find a way to set the user - auditLog.initActor(uuid, "MISSING", principal.getName()); - } - String method = containerRequestContext.getMethod(); - if (Objects.nonNull(method)) { - auditLog.initOperationMethod(uuid, method); - } - String path = containerRequestContext.getUriInfo().getPath(); - if (Objects.nonNull(path)) { - auditLog.initOperationPath(uuid, path); - } - // ToDo Check if headers should be set - MultivaluedMap headers = containerRequestContext.getHeaders(); - if (Objects.nonNull(headers)) { - auditLog.initOperationHeaders(uuid, headers); - } - // ToDo: Find a way to get status - if (LOGGER.isDebugEnabled() || LOGGER.isDebugEnabled(MARKER.REQUEST)) { LogContext.put(LogContext.CONTEXT.REQUEST, uuid); From 45453a20ca2a5f4d12b53ad35696b6e9d95525c2 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Wed, 27 May 2026 15:50:16 +0200 Subject: [PATCH 14/44] AuditLogResponseFilter for final logging --- .../base/app/AuditLogResponseFilter.java | 36 +++++++++++++++++++ .../base/domain/package-info.java | 6 +++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogResponseFilter.java diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogResponseFilter.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogResponseFilter.java new file mode 100644 index 00000000..cadfef8e --- /dev/null +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogResponseFilter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.base.app; + +import com.github.azahnen.dagger.annotations.AutoBind; +import de.ii.xtraplatform.base.domain.AuditLog; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import java.io.IOException; + +@Singleton +@AutoBind +public class AuditLogResponseFilter implements ContainerResponseFilter { + + private final AuditLog auditLog; + + @Inject + public AuditLogResponseFilter(AuditLog auditLog) { + this.auditLog = auditLog; + } + + @Override + public void filter( + ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + auditLog.saveLogToFileAndRemove(requestContext.getProperty("REQUEST_ID").toString()); + } +} diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/package-info.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/package-info.java index 30d4c275..3557e029 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/package-info.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/package-info.java @@ -1,4 +1,7 @@ -@AutoModule(single = true, encapsulate = true) +@AutoModule( + single = true, + encapsulate = true, + multiBindings = {ContainerResponseFilter.class}) @Value.Style( builder = "new", deepImmutablesDetection = true, @@ -8,5 +11,6 @@ package de.ii.xtraplatform.base.domain; import com.github.azahnen.dagger.annotations.AutoModule; +import jakarta.ws.rs.container.ContainerResponseFilter; import org.immutables.value.Value; import de.ii.xtraplatform.docs.DocIgnore; From 2c1ea6d2583073bcb921ed18cb78904f04dc8260 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Wed, 27 May 2026 17:38:29 +0200 Subject: [PATCH 15/44] init audit-log module --- xtraplatform-audit-log/build.gradle | 9 +++++++++ .../ii/xtraplatform/audit/log}/app/AuditLogImpl.java | 12 ++++++++---- .../audit/log}/app/AuditLogResponseFilter.java | 4 ++-- .../ii/xtraplatform/audit/log}/domain/AuditLog.java | 2 +- .../xtraplatform/audit/log/domain/package-info.java | 8 ++++++++ .../de/ii/xtraplatform/base/domain/package-info.java | 6 +----- 6 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 xtraplatform-audit-log/build.gradle rename {xtraplatform-base/src/main/java/de/ii/xtraplatform/base => xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log}/app/AuditLogImpl.java (93%) rename {xtraplatform-base/src/main/java/de/ii/xtraplatform/base => xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log}/app/AuditLogResponseFilter.java (91%) rename {xtraplatform-base/src/main/java/de/ii/xtraplatform/base => xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log}/domain/AuditLog.java (96%) create mode 100644 xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/package-info.java diff --git a/xtraplatform-audit-log/build.gradle b/xtraplatform-audit-log/build.gradle new file mode 100644 index 00000000..9111d661 --- /dev/null +++ b/xtraplatform-audit-log/build.gradle @@ -0,0 +1,9 @@ +// ToDo It is necessary to be mature and maintaned fully to compile +maturity = 'MATURE' +maintenance = 'FULL' +description = 'Audit logging for the Application' +descriptionDe = 'Audit logging für die Applikation.' + +dependencies { + provided project(':xtraplatform-blobs') +} \ No newline at end of file diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java similarity index 93% rename from xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java rename to xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java index 3e4b6cb7..6d9f90bf 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogImpl.java +++ b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java @@ -5,15 +5,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package de.ii.xtraplatform.base.app; +package de.ii.xtraplatform.audit.log.app; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.azahnen.dagger.annotations.AutoBind; -import de.ii.xtraplatform.base.domain.AuditLog; +import de.ii.xtraplatform.audit.log.domain.AuditLog; import de.ii.xtraplatform.base.domain.Jackson; +import de.ii.xtraplatform.blobs.domain.ResourceStore; import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.ws.rs.core.MultivaluedMap; @@ -32,10 +33,12 @@ public class AuditLogImpl implements AuditLog { private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogImpl.class); private final ObjectMapper objectMapper; private final Map auditLogMapping = new ConcurrentHashMap<>(); + ResourceStore auditLogStore; @Inject - AuditLogImpl(Jackson jackson) { + AuditLogImpl(Jackson jackson, ResourceStore resourceStore) { this.objectMapper = jackson.getDefaultObjectMapper(); + this.auditLogStore = resourceStore.writableWith("logs", "audit"); } private Log lazyInitOrGetAuditLog(String requestId) { @@ -83,7 +86,8 @@ public void saveLogToFileAndRemove(String requestId) throws JsonProcessingExcept if (LOGGER.isDebugEnabled()) { LOGGER.debug(objectMapper.writeValueAsString(auditLogMapping.get(requestId))); } - // ToDo save to file + // ToDo fix + // auditLogStore.put(Path.of(""), objectMapper.); auditLogMapping.remove(requestId); } else { if (LOGGER.isErrorEnabled()) { diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogResponseFilter.java b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java similarity index 91% rename from xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogResponseFilter.java rename to xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java index cadfef8e..8cb10b1c 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/app/AuditLogResponseFilter.java +++ b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java @@ -5,10 +5,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package de.ii.xtraplatform.base.app; +package de.ii.xtraplatform.audit.log.app; import com.github.azahnen.dagger.annotations.AutoBind; -import de.ii.xtraplatform.base.domain.AuditLog; +import de.ii.xtraplatform.audit.log.domain.AuditLog; import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.ws.rs.container.ContainerRequestContext; diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLog.java b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java similarity index 96% rename from xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLog.java rename to xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java index d9bb7789..580e14dc 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLog.java +++ b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java @@ -5,7 +5,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package de.ii.xtraplatform.base.domain; +package de.ii.xtraplatform.audit.log.domain; import com.fasterxml.jackson.core.JsonProcessingException; import jakarta.ws.rs.core.MultivaluedMap; diff --git a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/package-info.java b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/package-info.java new file mode 100644 index 00000000..12853134 --- /dev/null +++ b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/package-info.java @@ -0,0 +1,8 @@ +@AutoModule( + single = true, + encapsulate = true, + multiBindings = {ContainerResponseFilter.class}) +package de.ii.xtraplatform.audit.log.domain; + +import com.github.azahnen.dagger.annotations.AutoModule; +import jakarta.ws.rs.container.ContainerResponseFilter; diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/package-info.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/package-info.java index 3557e029..30d4c275 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/package-info.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/package-info.java @@ -1,7 +1,4 @@ -@AutoModule( - single = true, - encapsulate = true, - multiBindings = {ContainerResponseFilter.class}) +@AutoModule(single = true, encapsulate = true) @Value.Style( builder = "new", deepImmutablesDetection = true, @@ -11,6 +8,5 @@ package de.ii.xtraplatform.base.domain; import com.github.azahnen.dagger.annotations.AutoModule; -import jakarta.ws.rs.container.ContainerResponseFilter; import org.immutables.value.Value; import de.ii.xtraplatform.docs.DocIgnore; From 60a1bc6dbfd4a203475905035a63839c74eb0963 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Thu, 28 May 2026 09:54:58 +0200 Subject: [PATCH 16/44] decrease complexity of interface and implementation --- .../audit/log/app/AuditLogImpl.java | 157 ++++++++++-------- .../audit/log/app/AuditLogResponseFilter.java | 9 +- .../audit/log/domain/AuditLog.java | 45 +++-- 3 files changed, 124 insertions(+), 87 deletions(-) diff --git a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java index 6d9f90bf..b9b672dc 100644 --- a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java +++ b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java @@ -33,7 +33,7 @@ public class AuditLogImpl implements AuditLog { private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogImpl.class); private final ObjectMapper objectMapper; private final Map auditLogMapping = new ConcurrentHashMap<>(); - ResourceStore auditLogStore; + private final ResourceStore auditLogStore; @Inject AuditLogImpl(Jackson jackson, ResourceStore resourceStore) { @@ -46,49 +46,49 @@ private Log lazyInitOrGetAuditLog(String requestId) { } @Override - public void initApi(String requestId, String api) { - lazyInitOrGetAuditLog(requestId).initApi(api); + public void setApi(String requestId, String api) { + lazyInitOrGetAuditLog(requestId).setApi(api); } @Override - public void initActor(String requestId, String actorType, String actorId) { - lazyInitOrGetAuditLog(requestId).initActor(actorType, actorId); + public void setActor(String requestId, String actorType, String actorId) { + lazyInitOrGetAuditLog(requestId).setActor(actorType, actorId); } @Override - public void initOperationMethod(String requestId, String method) { - lazyInitOrGetAuditLog(requestId).initOperationMethod(method); + public void setOperationMethod(String requestId, String method) { + lazyInitOrGetAuditLog(requestId).setOperationMethod(method); } @Override - public void initOperationPath(String requestId, String path) { - lazyInitOrGetAuditLog(requestId).initOperationPath(path); + public void setOperationPath(String requestId, String path) { + lazyInitOrGetAuditLog(requestId).setOperationPath(path); } @Override - public void initOperationHeaders(String requestId, MultivaluedMap headers) { - lazyInitOrGetAuditLog(requestId).initOperationHeaders(headers); + public void setOperationHeaders(String requestId, MultivaluedMap headers) { + lazyInitOrGetAuditLog(requestId).setOperationHeaders(headers); } @Override - public void initOperationStatus(String requestId, String status) { - lazyInitOrGetAuditLog(requestId).initOperationStatus(status); + public void setOperationStatus(String requestId, String status) { + lazyInitOrGetAuditLog(requestId).setOperationStatus(status); } @Override - public void initTarget(String requestId, Map target) { - lazyInitOrGetAuditLog(requestId).initTarget(target); + public void setTarget(String requestId, Map target) { + lazyInitOrGetAuditLog(requestId).setTarget(target); } @Override public void saveLogToFileAndRemove(String requestId) throws JsonProcessingException { if (auditLogMapping.containsKey(requestId)) { + Log log = auditLogMapping.remove(requestId); + log.finish(); if (LOGGER.isDebugEnabled()) { - LOGGER.debug(objectMapper.writeValueAsString(auditLogMapping.get(requestId))); + LOGGER.debug(objectMapper.writeValueAsString(log)); } - // ToDo fix - // auditLogStore.put(Path.of(""), objectMapper.); - auditLogMapping.remove(requestId); + // ToDo: Save to file } else { if (LOGGER.isErrorEnabled()) { LOGGER.error("No AuditLog-object found for requestId {}", requestId); @@ -102,6 +102,7 @@ public static class LogImpl implements Log { private final Instant started; private final Map actor = new LinkedHashMap<>(); private final Map operation = new LinkedHashMap<>(); + private Instant finished; private Map target; private String api; @@ -110,95 +111,107 @@ public static class LogImpl implements Log { this.started = Instant.now(); } - @JsonProperty("id") - public String getId() { - return id; + @Override + public void finish() { + finished = Instant.now(); } - @JsonProperty("started") - public String getStarted() { - return started.toString(); + @Override + public void setApi(String api) { + if (Objects.isNull(this.api)) { + this.api = api; + } } - // This works but im still analysing if it could result in undesired behavior - @JsonProperty("finished") - public String finish() { - return Instant.now().toString(); + @Override + public void setActor(String actorType, String actorId) { + actor.put("type", actorType); + actor.put("id", actorId); } - @JsonProperty("api") - public String getApi() { - return api; + @Override + public void setOperationMethod(String method) { + operation.put("method", method); } - @JsonProperty("actor") - public Map getActor() { - return actor; + @Override + public void setOperationPath(String path) { + operation.put("path", path); } - @JsonProperty("operation") - public Map getOperation() { - return operation; - } + @Override + public void setOperationHeaders(MultivaluedMap headers) { + Map headersReduced = new HashMap<>(); - @JsonProperty("target") - public Map getTarget() { - return target; + headers.forEach( + (key, values) -> { + if (!values.isEmpty()) { + headersReduced.put(key, values.size() == 1 ? values.get(0) : values); + } + }); + + operation.put("headers", headersReduced); } @Override - public void initApi(String api) { - if (Objects.isNull(this.api)) { - this.api = api; - } + public void setOperationStatus(String status) { + operation.put("status", status); } @Override - public void initActor(String actorType, String actorId) { - actor.computeIfAbsent("type", k -> actorType); - actor.computeIfAbsent("id", k -> actorId); + public void setTarget(Map target) { + this.target = target; } + @JsonProperty("id") @Override - public void initOperationMethod(String method) { - operation.computeIfAbsent("method", k -> method); + public String getId() { + return id; } + @JsonProperty("started") @Override - public void initOperationPath(String path) { - operation.computeIfAbsent("path", k -> path); + public String getStarted() { + return started.toString(); } + @JsonProperty("finished") @Override - public void initOperationHeaders(MultivaluedMap headers) { - Map headersReduced = new HashMap<>(); - - for (String key : headers.keySet()) { - if (headers.get(key).isEmpty()) { - continue; - } - - if (headers.get(key).size() == 1) { - headersReduced.put(key, headers.get(key).get(0)); - continue; - } + public String getFinished() { + if (Objects.isNull(finished)) { + return ""; + } + return finished.toString(); + } - headersReduced.put(key, headers.get(key)); + @JsonProperty("api") + @Override + public String getApi() { + if (Objects.isNull(api)) { + return ""; } + return api; + } - operation.computeIfAbsent("headers", k -> headersReduced); + @JsonProperty("actor") + @Override + public Map getActor() { + return actor; } + @JsonProperty("operation") @Override - public void initOperationStatus(String status) { - operation.computeIfAbsent("status", k -> status); + public Map getOperation() { + return operation; } + @JsonProperty("target") @Override - public void initTarget(Map target) { - if (Objects.isNull(this.target)) { - this.target = target; + public Map getTarget() { + if (Objects.isNull(target)) { + return new LinkedHashMap<>(); } + return target; } } } diff --git a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java index 8cb10b1c..b2c9f1e4 100644 --- a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java +++ b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java @@ -15,6 +15,7 @@ import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; import java.io.IOException; +import java.util.Objects; @Singleton @AutoBind @@ -31,6 +32,12 @@ public AuditLogResponseFilter(AuditLog auditLog) { public void filter( ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { - auditLog.saveLogToFileAndRemove(requestContext.getProperty("REQUEST_ID").toString()); + if (Objects.nonNull(requestContext)) { + String requestId = requestContext.getProperty("REQUEST_ID").toString(); + if (Objects.nonNull(responseContext)) { + auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); + } + auditLog.saveLogToFileAndRemove(requestId); + } } } diff --git a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java index 580e14dc..3cdadca9 100644 --- a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java +++ b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java @@ -12,35 +12,52 @@ import java.util.Map; public interface AuditLog { - void initApi(String requestId, String api); + void setApi(String requestId, String api); - void initActor(String requestId, String actorType, String actorId); + void setActor(String requestId, String actorType, String actorId); - void initOperationMethod(String requestId, String method); + void setOperationMethod(String requestId, String method); - void initOperationPath(String requestId, String path); + void setOperationPath(String requestId, String path); - void initOperationHeaders(String requestId, MultivaluedMap headers); + void setOperationHeaders(String requestId, MultivaluedMap headers); - void initOperationStatus(String requestId, String status); + void setOperationStatus(String requestId, String status); - void initTarget(String requestId, Map target); + void setTarget(String requestId, Map target); void saveLogToFileAndRemove(String requestId) throws JsonProcessingException; interface Log { - void initApi(String api); - void initActor(String actorType, String actorId); + void finish(); - void initOperationMethod(String method); + void setApi(String api); - void initOperationPath(String path); + void setActor(String actorType, String actorId); - void initOperationHeaders(MultivaluedMap headers); + void setOperationMethod(String method); - void initOperationStatus(String status); + void setOperationPath(String path); - void initTarget(Map target); + void setOperationHeaders(MultivaluedMap headers); + + void setOperationStatus(String status); + + void setTarget(Map target); + + String getId(); + + String getStarted(); + + String getFinished(); + + String getApi(); + + Map getActor(); + + Map getOperation(); + + Map getTarget(); } } From 03cb3f7f6e547fdb4dff2a418371e8b934201061 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Thu, 28 May 2026 10:46:20 +0200 Subject: [PATCH 17/44] write logs with ResourceStore --- .../audit/log/app/AuditLogImpl.java | 32 +++++++++++++------ .../audit/log/app/AuditLogResponseFilter.java | 2 +- .../audit/log/domain/AuditLog.java | 3 +- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java index b9b672dc..2be177a6 100644 --- a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java +++ b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java @@ -18,6 +18,10 @@ import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.ws.rs.core.MultivaluedMap; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; import java.time.Instant; import java.util.HashMap; import java.util.LinkedHashMap; @@ -81,18 +85,28 @@ public void setTarget(String requestId, Map target) { } @Override - public void saveLogToFileAndRemove(String requestId) throws JsonProcessingException { - if (auditLogMapping.containsKey(requestId)) { - Log log = auditLogMapping.remove(requestId); - log.finish(); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug(objectMapper.writeValueAsString(log)); - } - // ToDo: Save to file - } else { + public void writeAndRemoveLog(String requestId) { + Log log = auditLogMapping.remove(requestId); + if (Objects.isNull(log)) { if (LOGGER.isErrorEnabled()) { LOGGER.error("No AuditLog-object found for requestId {}", requestId); } + return; + } + + log.finish(); + + try { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(objectMapper.writeValueAsString(log)); + } + auditLogStore.put( + Path.of(log.getStarted() + "_" + requestId + ".json"), + new ByteArrayInputStream(objectMapper.writeValueAsBytes(log))); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Failed to serialize log " + requestId, e); + } catch (IOException e) { + throw new UncheckedIOException("Failed to write log " + requestId, e); } } diff --git a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java index b2c9f1e4..4d9b0a97 100644 --- a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java +++ b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java @@ -37,7 +37,7 @@ public void filter( if (Objects.nonNull(responseContext)) { auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); } - auditLog.saveLogToFileAndRemove(requestId); + auditLog.writeAndRemoveLog(requestId); } } } diff --git a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java index 3cdadca9..31d74e36 100644 --- a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java +++ b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java @@ -7,7 +7,6 @@ */ package de.ii.xtraplatform.audit.log.domain; -import com.fasterxml.jackson.core.JsonProcessingException; import jakarta.ws.rs.core.MultivaluedMap; import java.util.Map; @@ -26,7 +25,7 @@ public interface AuditLog { void setTarget(String requestId, Map target); - void saveLogToFileAndRemove(String requestId) throws JsonProcessingException; + void writeAndRemoveLog(String requestId); interface Log { From baee5d0193487f16760a7d04f6b19326ac2b3056 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Fri, 29 May 2026 13:16:49 +0200 Subject: [PATCH 18/44] move to xtraplatform-services --- xtraplatform-audit-log/build.gradle | 9 ------- .../audit/log/domain/package-info.java | 8 ------ xtraplatform-services/build.gradle | 2 +- .../services}/app/AuditLogImpl.java | 13 ++-------- .../services}/app/AuditLogResponseFilter.java | 25 ++++++++++++------- .../services}/domain/AuditLog.java | 2 +- 6 files changed, 20 insertions(+), 39 deletions(-) delete mode 100644 xtraplatform-audit-log/build.gradle delete mode 100644 xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/package-info.java rename {xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log => xtraplatform-services/src/main/java/de/ii/xtraplatform/services}/app/AuditLogImpl.java (95%) rename {xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log => xtraplatform-services/src/main/java/de/ii/xtraplatform/services}/app/AuditLogResponseFilter.java (60%) rename {xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log => xtraplatform-services/src/main/java/de/ii/xtraplatform/services}/domain/AuditLog.java (96%) diff --git a/xtraplatform-audit-log/build.gradle b/xtraplatform-audit-log/build.gradle deleted file mode 100644 index 9111d661..00000000 --- a/xtraplatform-audit-log/build.gradle +++ /dev/null @@ -1,9 +0,0 @@ -// ToDo It is necessary to be mature and maintaned fully to compile -maturity = 'MATURE' -maintenance = 'FULL' -description = 'Audit logging for the Application' -descriptionDe = 'Audit logging für die Applikation.' - -dependencies { - provided project(':xtraplatform-blobs') -} \ No newline at end of file diff --git a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/package-info.java b/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/package-info.java deleted file mode 100644 index 12853134..00000000 --- a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -@AutoModule( - single = true, - encapsulate = true, - multiBindings = {ContainerResponseFilter.class}) -package de.ii.xtraplatform.audit.log.domain; - -import com.github.azahnen.dagger.annotations.AutoModule; -import jakarta.ws.rs.container.ContainerResponseFilter; diff --git a/xtraplatform-services/build.gradle b/xtraplatform-services/build.gradle index cd946c3d..49bcf4c7 100644 --- a/xtraplatform-services/build.gradle +++ b/xtraplatform-services/build.gradle @@ -1,4 +1,3 @@ - maturity = 'MATURE' maintenance = 'FULL' description = 'Service entities, background tasks.' @@ -9,6 +8,7 @@ dependencies { provided project(':xtraplatform-entities') provided project(':xtraplatform-values') provided project(':xtraplatform-openapi') + provided project(':xtraplatform-blobs') embedded libs.cron4j } diff --git a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java similarity index 95% rename from xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java rename to xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 2be177a6..6c6a6060 100644 --- a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -5,14 +5,14 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package de.ii.xtraplatform.audit.log.app; +package de.ii.xtraplatform.services.app; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.azahnen.dagger.annotations.AutoBind; -import de.ii.xtraplatform.audit.log.domain.AuditLog; +import de.ii.xtraplatform.services.domain.AuditLog; import de.ii.xtraplatform.base.domain.Jackson; import de.ii.xtraplatform.blobs.domain.ResourceStore; import jakarta.inject.Inject; @@ -192,18 +192,12 @@ public String getStarted() { @JsonProperty("finished") @Override public String getFinished() { - if (Objects.isNull(finished)) { - return ""; - } return finished.toString(); } @JsonProperty("api") @Override public String getApi() { - if (Objects.isNull(api)) { - return ""; - } return api; } @@ -222,9 +216,6 @@ public Map getOperation() { @JsonProperty("target") @Override public Map getTarget() { - if (Objects.isNull(target)) { - return new LinkedHashMap<>(); - } return target; } } diff --git a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java similarity index 60% rename from xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java rename to xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java index 4d9b0a97..66102e9b 100644 --- a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/app/AuditLogResponseFilter.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java @@ -5,10 +5,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package de.ii.xtraplatform.audit.log.app; +package de.ii.xtraplatform.services.app; import com.github.azahnen.dagger.annotations.AutoBind; -import de.ii.xtraplatform.audit.log.domain.AuditLog; +import de.ii.xtraplatform.services.domain.AuditLog; import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.ws.rs.container.ContainerRequestContext; @@ -16,11 +16,13 @@ import jakarta.ws.rs.container.ContainerResponseFilter; import java.io.IOException; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Singleton @AutoBind public class AuditLogResponseFilter implements ContainerResponseFilter { - + private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogResponseFilter.class); private final AuditLog auditLog; @Inject @@ -32,12 +34,17 @@ public AuditLogResponseFilter(AuditLog auditLog) { public void filter( ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { - if (Objects.nonNull(requestContext)) { - String requestId = requestContext.getProperty("REQUEST_ID").toString(); - if (Objects.nonNull(responseContext)) { - auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); - } - auditLog.writeAndRemoveLog(requestId); + if (Objects.isNull(requestContext) || Objects.isNull(responseContext)) { + return; + } + + Object requestIdObject = requestContext.getProperty("REQUEST_ID"); + if (Objects.isNull(requestIdObject)) { + return; } + + String requestId = requestIdObject.toString(); + auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); + auditLog.writeAndRemoveLog(requestId); } } diff --git a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java similarity index 96% rename from xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java rename to xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java index 31d74e36..eab1b3b5 100644 --- a/xtraplatform-audit-log/src/main/java/de/ii/xtraplatform/audit/log/domain/AuditLog.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java @@ -5,7 +5,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package de.ii.xtraplatform.audit.log.domain; +package de.ii.xtraplatform.services.domain; import jakarta.ws.rs.core.MultivaluedMap; import java.util.Map; From 0775fa5ddc29afe6547634def6616476b3bc5903 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Fri, 29 May 2026 13:17:25 +0200 Subject: [PATCH 19/44] move to xtraplatform-services --- .../main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 6c6a6060..cd59fe63 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -12,9 +12,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.azahnen.dagger.annotations.AutoBind; -import de.ii.xtraplatform.services.domain.AuditLog; import de.ii.xtraplatform.base.domain.Jackson; import de.ii.xtraplatform.blobs.domain.ResourceStore; +import de.ii.xtraplatform.services.domain.AuditLog; import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.ws.rs.core.MultivaluedMap; From d69ce7ed6fbc67c7dd08b0da41c1b77735196dae Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Fri, 29 May 2026 14:37:12 +0200 Subject: [PATCH 20/44] init work on global config --- .../base/domain/AppConfiguration.java | 4 ++++ .../base/domain/AuditLogConfiguration.java | 22 +++++++++++++++++++ .../src/main/resources/cfg.base.yml | 5 ++++- .../services/app/AuditLogImpl.java | 5 ++++- 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AppConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AppConfiguration.java index 79f71579..7fa2af08 100755 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AppConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AppConfiguration.java @@ -151,6 +151,10 @@ && getJobs().getMaxConcurrent() == 1) { @Valid public abstract RedisConfiguration getRedis(); + @JsonProperty("auditLog") + @Valid + public abstract AuditLogConfiguration getAuditLog(); + @JsonIgnore @Override public void setServerFactory(ServerFactory factory) { diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java new file mode 100644 index 00000000..c6fe163e --- /dev/null +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java @@ -0,0 +1,22 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.base.domain; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.immutables.value.Value; + +@Value.Immutable +@Value.Modifiable +@JsonDeserialize(as = ModifiableAuditLogConfiguration.class) +public interface AuditLogConfiguration { + + @Value.Default + default boolean getEnabled() { + return false; + } +} diff --git a/xtraplatform-base/src/main/resources/cfg.base.yml b/xtraplatform-base/src/main/resources/cfg.base.yml index ce61bb69..1cf35b99 100644 --- a/xtraplatform-base/src/main/resources/cfg.base.yml +++ b/xtraplatform-base/src/main/resources/cfg.base.yml @@ -68,4 +68,7 @@ logging: metrics: frequency: 1 minute reporters: [ ] - reportOnStop: false \ No newline at end of file + reportOnStop: false + +auditLog: + enabled: false \ No newline at end of file diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index cd59fe63..e5bbf1e5 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.azahnen.dagger.annotations.AutoBind; +import de.ii.xtraplatform.base.domain.AppContext; import de.ii.xtraplatform.base.domain.Jackson; import de.ii.xtraplatform.blobs.domain.ResourceStore; import de.ii.xtraplatform.services.domain.AuditLog; @@ -38,11 +39,13 @@ public class AuditLogImpl implements AuditLog { private final ObjectMapper objectMapper; private final Map auditLogMapping = new ConcurrentHashMap<>(); private final ResourceStore auditLogStore; + private final AppContext appContext; @Inject - AuditLogImpl(Jackson jackson, ResourceStore resourceStore) { + AuditLogImpl(Jackson jackson, ResourceStore resourceStore, AppContext appContext) { this.objectMapper = jackson.getDefaultObjectMapper(); this.auditLogStore = resourceStore.writableWith("logs", "audit"); + this.appContext = appContext; } private Log lazyInitOrGetAuditLog(String requestId) { From 4118c2f1d838ed0f54d39c7eb2d2bd3fb4a3500e Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 2 Jun 2026 10:12:59 +0200 Subject: [PATCH 21/44] explicit log creation --- .../services/app/AuditLogImpl.java | 37 ++++++++++++------- .../services/app/ServicesEndpoint.java | 8 +++- .../services/domain/AuditLog.java | 2 + 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index e5bbf1e5..66288984 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -28,6 +28,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,43 +49,53 @@ public class AuditLogImpl implements AuditLog { this.appContext = appContext; } - private Log lazyInitOrGetAuditLog(String requestId) { - return auditLogMapping.computeIfAbsent(requestId, k -> new LogImpl(requestId)); + private Optional getOptionalLog(String requestId) { + if (auditLogMapping.containsKey(requestId)) { + return Optional.of(auditLogMapping.get(requestId)); + } else { + LOGGER.error("No AuditLog-object found for requestId {}", requestId); + return Optional.empty(); + } + } + + @Override + public void createLog(String requestId) { + auditLogMapping.computeIfAbsent(requestId, k -> new LogImpl(requestId)); } @Override public void setApi(String requestId, String api) { - lazyInitOrGetAuditLog(requestId).setApi(api); + getOptionalLog(requestId).ifPresent(log -> log.setApi(api)); } @Override public void setActor(String requestId, String actorType, String actorId) { - lazyInitOrGetAuditLog(requestId).setActor(actorType, actorId); + getOptionalLog(requestId).ifPresent(log -> log.setActor(actorType, actorId)); } @Override public void setOperationMethod(String requestId, String method) { - lazyInitOrGetAuditLog(requestId).setOperationMethod(method); + getOptionalLog(requestId).ifPresent(log -> log.setOperationMethod(method)); } @Override public void setOperationPath(String requestId, String path) { - lazyInitOrGetAuditLog(requestId).setOperationPath(path); + getOptionalLog(requestId).ifPresent(log -> log.setOperationPath(path)); } @Override public void setOperationHeaders(String requestId, MultivaluedMap headers) { - lazyInitOrGetAuditLog(requestId).setOperationHeaders(headers); + getOptionalLog(requestId).ifPresent(log -> log.setOperationHeaders(headers)); } @Override public void setOperationStatus(String requestId, String status) { - lazyInitOrGetAuditLog(requestId).setOperationStatus(status); + getOptionalLog(requestId).ifPresent(log -> log.setOperationStatus(status)); } @Override public void setTarget(String requestId, Map target) { - lazyInitOrGetAuditLog(requestId).setTarget(target); + getOptionalLog(requestId).ifPresent(log -> log.setTarget(target)); } @Override @@ -104,7 +115,7 @@ public void writeAndRemoveLog(String requestId) { LOGGER.debug(objectMapper.writeValueAsString(log)); } auditLogStore.put( - Path.of(log.getStarted() + "_" + requestId + ".json"), + Path.of(log.getStarted().substring(0, 10) + "_" + requestId + ".json"), new ByteArrayInputStream(objectMapper.writeValueAsBytes(log))); } catch (JsonProcessingException e) { throw new IllegalStateException("Failed to serialize log " + requestId, e); @@ -135,9 +146,7 @@ public void finish() { @Override public void setApi(String api) { - if (Objects.isNull(this.api)) { - this.api = api; - } + this.api = api; } @Override @@ -177,7 +186,7 @@ public void setOperationStatus(String status) { @Override public void setTarget(Map target) { - this.target = target; + this.target = new LinkedHashMap<>(target); } @JsonProperty("id") diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java index 3cb6b4d5..add1afd2 100755 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/ServicesEndpoint.java @@ -13,6 +13,7 @@ import de.ii.xtraplatform.base.domain.LogContext; import de.ii.xtraplatform.base.domain.LogContext.MARKER; import de.ii.xtraplatform.entities.domain.EntityRegistry; +import de.ii.xtraplatform.services.domain.AuditLog; import de.ii.xtraplatform.services.domain.Service; import de.ii.xtraplatform.services.domain.ServiceData; import de.ii.xtraplatform.services.domain.ServiceEndpoint; @@ -79,6 +80,8 @@ public class ServicesEndpoint implements Endpoint { private final Lazy> serviceResources; private final Lazy> serviceListingProviders; + private final AuditLog auditLog; + @Inject public ServicesEndpoint( EntityRegistry entityRegistry, @@ -87,7 +90,8 @@ public ServicesEndpoint( StaticResourceHandler staticResourceHandler, Lazy> loginHandler, Lazy> serviceResources, - Lazy> serviceListingProviders) { + Lazy> serviceListingProviders, + AuditLog auditLog) { this.entityRegistry = entityRegistry; this.servicesContext = servicesContext; this.serviceContext = serviceContext; @@ -95,6 +99,7 @@ public ServicesEndpoint( this.loginHandler = loginHandler; this.serviceResources = serviceResources; this.serviceListingProviders = serviceListingProviders; + this.auditLog = auditLog; } @GET @@ -360,6 +365,7 @@ private void setupRequestLogging( String serviceId, Integer version, ContainerRequestContext containerRequestContext) { String uuid = LogContext.generateRandomUuid().toString(); containerRequestContext.setProperty("REQUEST_ID", uuid); + auditLog.createLog(uuid); if (LOGGER.isDebugEnabled() || LOGGER.isDebugEnabled(MARKER.REQUEST)) { LogContext.put(LogContext.CONTEXT.REQUEST, uuid); diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java index eab1b3b5..c2c14749 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java @@ -11,6 +11,8 @@ import java.util.Map; public interface AuditLog { + void createLog(String requestId); + void setApi(String requestId, String api); void setActor(String requestId, String actorType, String actorId); From a12eed3d51b4c583b8405407236ae085a37ebbae Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 2 Jun 2026 12:24:03 +0200 Subject: [PATCH 22/44] global config: retries --- .../base/domain/AuditLogConfiguration.java | 5 ++ .../src/main/resources/cfg.base.yml | 3 +- .../services/app/AuditLogImpl.java | 84 +++++++++++++++++-- .../services/app/AuditLogResponseFilter.java | 10 ++- .../services/domain/AuditLog.java | 2 +- 5 files changed, 92 insertions(+), 12 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java index c6fe163e..5a73e899 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java @@ -19,4 +19,9 @@ public interface AuditLogConfiguration { default boolean getEnabled() { return false; } + + @Value.Default + default int getRetries() { + return 3; + } } diff --git a/xtraplatform-base/src/main/resources/cfg.base.yml b/xtraplatform-base/src/main/resources/cfg.base.yml index 1cf35b99..2801a79c 100644 --- a/xtraplatform-base/src/main/resources/cfg.base.yml +++ b/xtraplatform-base/src/main/resources/cfg.base.yml @@ -71,4 +71,5 @@ metrics: reportOnStop: false auditLog: - enabled: false \ No newline at end of file + enabled: false + retries: 3 \ No newline at end of file diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 66288984..656867f1 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -21,9 +21,10 @@ import jakarta.ws.rs.core.MultivaluedMap; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.file.Path; import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @@ -58,70 +59,135 @@ private Optional getOptionalLog(String requestId) { } } + private boolean isDisabled() { + return !appContext.getConfiguration().getAuditLog().getEnabled(); + } + + private Path getPath(String requestId, Log log) { + // ToDo: Custom Path + String isoDate = + DateTimeFormatter.ISO_LOCAL_DATE + .withZone(ZoneOffset.UTC) + .format(Instant.parse(log.getStarted())); + return Path.of(isoDate + "_" + requestId + ".json"); + } + @Override public void createLog(String requestId) { + if (isDisabled()) { + return; + } auditLogMapping.computeIfAbsent(requestId, k -> new LogImpl(requestId)); } @Override public void setApi(String requestId, String api) { + if (isDisabled()) { + return; + } getOptionalLog(requestId).ifPresent(log -> log.setApi(api)); } @Override public void setActor(String requestId, String actorType, String actorId) { + if (isDisabled()) { + return; + } getOptionalLog(requestId).ifPresent(log -> log.setActor(actorType, actorId)); } @Override public void setOperationMethod(String requestId, String method) { + if (isDisabled()) { + return; + } getOptionalLog(requestId).ifPresent(log -> log.setOperationMethod(method)); } @Override public void setOperationPath(String requestId, String path) { + if (isDisabled()) { + return; + } getOptionalLog(requestId).ifPresent(log -> log.setOperationPath(path)); } @Override public void setOperationHeaders(String requestId, MultivaluedMap headers) { + if (isDisabled()) { + return; + } getOptionalLog(requestId).ifPresent(log -> log.setOperationHeaders(headers)); } @Override public void setOperationStatus(String requestId, String status) { + if (isDisabled()) { + return; + } getOptionalLog(requestId).ifPresent(log -> log.setOperationStatus(status)); } @Override public void setTarget(String requestId, Map target) { + if (isDisabled()) { + return; + } getOptionalLog(requestId).ifPresent(log -> log.setTarget(target)); } @Override - public void writeAndRemoveLog(String requestId) { + @SuppressWarnings({"PMD.CognitiveComplexity", "PMD.CyclomaticComplexity"}) + public boolean writeAndRemoveLog(String requestId) { + if (isDisabled()) { + return false; + } + Log log = auditLogMapping.remove(requestId); if (Objects.isNull(log)) { if (LOGGER.isErrorEnabled()) { LOGGER.error("No AuditLog-object found for requestId {}", requestId); } - return; + return false; } log.finish(); + final ByteArrayInputStream inputStream; try { if (LOGGER.isDebugEnabled()) { LOGGER.debug(objectMapper.writeValueAsString(log)); } - auditLogStore.put( - Path.of(log.getStarted().substring(0, 10) + "_" + requestId + ".json"), - new ByteArrayInputStream(objectMapper.writeValueAsBytes(log))); + inputStream = new ByteArrayInputStream(objectMapper.writeValueAsBytes(log)); } catch (JsonProcessingException e) { - throw new IllegalStateException("Failed to serialize log " + requestId, e); - } catch (IOException e) { - throw new UncheckedIOException("Failed to write log " + requestId, e); + LOGGER.error("Failed to serialize log " + requestId, e); + return false; } + + int maxRetries = appContext.getConfiguration().getAuditLog().getRetries(); + Path path = getPath(requestId, log); + + int retries = 0; + do { + try { + inputStream.reset(); + auditLogStore.put(path, inputStream); + return true; + } catch (IOException e) { + // ToDo: Delay until next try + if (retries < maxRetries) { + if (LOGGER.isWarnEnabled()) { + LOGGER.warn("Failed to write audit log {}, retrying...", requestId); + } + } else { + LOGGER.error("Giving up writing audit log {} after {} retries", requestId, retries, e); + return false; + } + } + retries++; + } while (retries <= maxRetries); + + return false; } @JsonPropertyOrder({"id", "started", "finished", "api", "actor", "operation", "target"}) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java index 66102e9b..9aa24fb3 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java @@ -8,6 +8,7 @@ package de.ii.xtraplatform.services.app; import com.github.azahnen.dagger.annotations.AutoBind; +import de.ii.xtraplatform.base.domain.AppContext; import de.ii.xtraplatform.services.domain.AuditLog; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -24,16 +25,22 @@ public class AuditLogResponseFilter implements ContainerResponseFilter { private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogResponseFilter.class); private final AuditLog auditLog; + private final AppContext appContext; @Inject - public AuditLogResponseFilter(AuditLog auditLog) { + public AuditLogResponseFilter(AuditLog auditLog, AppContext appContext) { this.auditLog = auditLog; + this.appContext = appContext; } @Override public void filter( ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { + if (!appContext.getConfiguration().getAuditLog().getEnabled()) { + return; + } + if (Objects.isNull(requestContext) || Objects.isNull(responseContext)) { return; } @@ -45,6 +52,7 @@ public void filter( String requestId = requestIdObject.toString(); auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); + // ToDo: Abort response if false is returned! auditLog.writeAndRemoveLog(requestId); } } diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java index c2c14749..ac61af7c 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java @@ -27,7 +27,7 @@ public interface AuditLog { void setTarget(String requestId, Map target); - void writeAndRemoveLog(String requestId); + boolean writeAndRemoveLog(String requestId); interface Log { From 5379717b873837b68a88d211152693334552a27f Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 2 Jun 2026 14:59:20 +0200 Subject: [PATCH 23/44] global config: pathPrefix --- .../base/domain/AuditLogConfiguration.java | 5 +++++ .../src/main/resources/cfg.base.yml | 3 ++- .../xtraplatform/services/app/AuditLogImpl.java | 16 ++++++++++++---- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java index 5a73e899..aaa51c8b 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java @@ -24,4 +24,9 @@ default boolean getEnabled() { default int getRetries() { return 3; } + + @Value.Default + default String getPathPrefix() { + return "{api}/{date}"; + } } diff --git a/xtraplatform-base/src/main/resources/cfg.base.yml b/xtraplatform-base/src/main/resources/cfg.base.yml index 2801a79c..c98033b3 100644 --- a/xtraplatform-base/src/main/resources/cfg.base.yml +++ b/xtraplatform-base/src/main/resources/cfg.base.yml @@ -72,4 +72,5 @@ metrics: auditLog: enabled: false - retries: 3 \ No newline at end of file + retries: 3 + pathPrefix: "{api}/{date}" \ No newline at end of file diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 656867f1..789c32bb 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -63,13 +63,21 @@ private boolean isDisabled() { return !appContext.getConfiguration().getAuditLog().getEnabled(); } - private Path getPath(String requestId, Log log) { - // ToDo: Custom Path + private Path createPath(String requestId, Log log) { + String pathPrefix = appContext.getConfiguration().getAuditLog().getPathPrefix(); + + // Replace api + String api = Objects.isNull(log.getApi()) ? "homepage" : log.getApi(); + pathPrefix = pathPrefix.replace("{api}", api); + + // Replace date String isoDate = DateTimeFormatter.ISO_LOCAL_DATE .withZone(ZoneOffset.UTC) .format(Instant.parse(log.getStarted())); - return Path.of(isoDate + "_" + requestId + ".json"); + pathPrefix = pathPrefix.replace("{date}", isoDate); + + return Path.of(pathPrefix).resolve(Path.of(requestId + ".json")); } @Override @@ -165,7 +173,7 @@ public boolean writeAndRemoveLog(String requestId) { } int maxRetries = appContext.getConfiguration().getAuditLog().getRetries(); - Path path = getPath(requestId, log); + Path path = createPath(requestId, log); int retries = 0; do { From 113497280a333bac86bf22b31dbae01f4e01bf92 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 2 Jun 2026 15:41:30 +0200 Subject: [PATCH 24/44] global config: type --- .../base/domain/AuditLogConfiguration.java | 10 ++++++ .../src/main/resources/cfg.base.yml | 3 +- .../services/app/AuditLogImpl.java | 35 ++++++++++++------- .../services/domain/AuditLog.java | 5 +-- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java index aaa51c8b..f197447c 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java @@ -15,6 +15,11 @@ @JsonDeserialize(as = ModifiableAuditLogConfiguration.class) public interface AuditLogConfiguration { + enum TYPE { + JSON, + JSON_PRETTY + } + @Value.Default default boolean getEnabled() { return false; @@ -29,4 +34,9 @@ default int getRetries() { default String getPathPrefix() { return "{api}/{date}"; } + + @Value.Default + default TYPE getType() { + return TYPE.JSON; + } } diff --git a/xtraplatform-base/src/main/resources/cfg.base.yml b/xtraplatform-base/src/main/resources/cfg.base.yml index c98033b3..d423f9a3 100644 --- a/xtraplatform-base/src/main/resources/cfg.base.yml +++ b/xtraplatform-base/src/main/resources/cfg.base.yml @@ -73,4 +73,5 @@ metrics: auditLog: enabled: false retries: 3 - pathPrefix: "{api}/{date}" \ No newline at end of file + type: ${TYPE:-JSON} + pathPrefix: '{api}/{date}' \ No newline at end of file diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 789c32bb..3a04e8f0 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -10,9 +10,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializationFeature; import com.github.azahnen.dagger.annotations.AutoBind; import de.ii.xtraplatform.base.domain.AppContext; +import de.ii.xtraplatform.base.domain.AuditLogConfiguration; import de.ii.xtraplatform.base.domain.Jackson; import de.ii.xtraplatform.blobs.domain.ResourceStore; import de.ii.xtraplatform.services.domain.AuditLog; @@ -38,14 +40,25 @@ @AutoBind public class AuditLogImpl implements AuditLog { private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogImpl.class); - private final ObjectMapper objectMapper; + private ObjectWriter objectWriter; private final Map auditLogMapping = new ConcurrentHashMap<>(); private final ResourceStore auditLogStore; private final AppContext appContext; @Inject AuditLogImpl(Jackson jackson, ResourceStore resourceStore, AppContext appContext) { - this.objectMapper = jackson.getDefaultObjectMapper(); + objectWriter = + jackson + .getDefaultObjectMapper() + .writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + if (appContext.getConfiguration().getAuditLog().getType() == AuditLogConfiguration.TYPE.JSON) { + objectWriter = objectWriter.without(SerializationFeature.INDENT_OUTPUT); + } else { + objectWriter = objectWriter.with(SerializationFeature.INDENT_OUTPUT); + } + this.auditLogStore = resourceStore.writableWith("logs", "audit"); this.appContext = appContext; } @@ -72,9 +85,7 @@ private Path createPath(String requestId, Log log) { // Replace date String isoDate = - DateTimeFormatter.ISO_LOCAL_DATE - .withZone(ZoneOffset.UTC) - .format(Instant.parse(log.getStarted())); + DateTimeFormatter.ISO_LOCAL_DATE.withZone(ZoneOffset.UTC).format(log.getStarted()); pathPrefix = pathPrefix.replace("{date}", isoDate); return Path.of(pathPrefix).resolve(Path.of(requestId + ".json")); @@ -164,9 +175,9 @@ public boolean writeAndRemoveLog(String requestId) { final ByteArrayInputStream inputStream; try { if (LOGGER.isDebugEnabled()) { - LOGGER.debug(objectMapper.writeValueAsString(log)); + LOGGER.debug(objectWriter.writeValueAsString(log)); } - inputStream = new ByteArrayInputStream(objectMapper.writeValueAsBytes(log)); + inputStream = new ByteArrayInputStream(objectWriter.writeValueAsBytes(log)); } catch (JsonProcessingException e) { LOGGER.error("Failed to serialize log " + requestId, e); return false; @@ -271,14 +282,14 @@ public String getId() { @JsonProperty("started") @Override - public String getStarted() { - return started.toString(); + public Instant getStarted() { + return started; } @JsonProperty("finished") @Override - public String getFinished() { - return finished.toString(); + public Instant getFinished() { + return finished; } @JsonProperty("api") diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java index ac61af7c..9c55d387 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java @@ -8,6 +8,7 @@ package de.ii.xtraplatform.services.domain; import jakarta.ws.rs.core.MultivaluedMap; +import java.time.Instant; import java.util.Map; public interface AuditLog { @@ -49,9 +50,9 @@ interface Log { String getId(); - String getStarted(); + Instant getStarted(); - String getFinished(); + Instant getFinished(); String getApi(); From ab6147d2afb5092aaf667f22e44dd3ac36153d50 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 2 Jun 2026 17:18:03 +0200 Subject: [PATCH 25/44] global config: type (without default values) --- .../base/domain/AuditLogConfiguration.java | 42 +++++++++++++++---- .../src/main/resources/cfg.base.yml | 7 +++- .../services/app/AuditLogImpl.java | 24 +++++++++-- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java index f197447c..eb6acf8e 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java @@ -8,35 +8,59 @@ package de.ii.xtraplatform.base.domain; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; import org.immutables.value.Value; +import org.immutables.value.Value.Default; @Value.Immutable @Value.Modifiable @JsonDeserialize(as = ModifiableAuditLogConfiguration.class) public interface AuditLogConfiguration { - enum TYPE { - JSON, - JSON_PRETTY - } - - @Value.Default + @Default default boolean getEnabled() { return false; } - @Value.Default + @Default default int getRetries() { return 3; } - @Value.Default + @Default default String getPathPrefix() { return "{api}/{date}"; } - @Value.Default + @Default default TYPE getType() { return TYPE.JSON; } + + @Default + default HeaderConfiguration getHeaders() { + return ModifiableHeaderConfiguration.create(); + } + + enum TYPE { + JSON, + JSON_PRETTY + } + + @Value.Immutable + @Value.Modifiable + @JsonDeserialize(as = ModifiableHeaderConfiguration.class) + interface HeaderConfiguration { + @Value.Default + default List getIncluded() { + // Find out how to stop default values to merge with custom values + // return List.of("*"); + return List.of(""); + } + + @Value.Default + default List getExcluded() { + return List.of(""); + } + } } diff --git a/xtraplatform-base/src/main/resources/cfg.base.yml b/xtraplatform-base/src/main/resources/cfg.base.yml index d423f9a3..b78b0019 100644 --- a/xtraplatform-base/src/main/resources/cfg.base.yml +++ b/xtraplatform-base/src/main/resources/cfg.base.yml @@ -74,4 +74,9 @@ auditLog: enabled: false retries: 3 type: ${TYPE:-JSON} - pathPrefix: '{api}/{date}' \ No newline at end of file + pathPrefix: '{api}/{date}' + headers: + # Find out how to stop default values to merge with custom values + # included: [ '*' ] + included: [ ] + excluded: [ ] \ No newline at end of file diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 3a04e8f0..b7da6aca 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -20,6 +20,7 @@ import de.ii.xtraplatform.services.domain.AuditLog; import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -27,8 +28,8 @@ import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; -import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -136,7 +137,24 @@ public void setOperationHeaders(String requestId, MultivaluedMap if (isDisabled()) { return; } - getOptionalLog(requestId).ifPresent(log -> log.setOperationHeaders(headers)); + + List includes = appContext.getConfiguration().getAuditLog().getHeaders().getIncluded(); + LOGGER.error(includes.toString()); + List excludes = appContext.getConfiguration().getAuditLog().getHeaders().getExcluded(); + LOGGER.error(excludes.toString()); + MultivaluedMap headersFiltered = new MultivaluedHashMap<>(); + + headers.forEach( + (k, v) -> { + LOGGER.error(k); + if (!excludes.contains(k) + && !excludes.contains("*") + && (includes.contains("*") || includes.contains(k))) { + headersFiltered.addAll(k, v); + } + }); + + getOptionalLog(requestId).ifPresent(log -> log.setOperationHeaders(headersFiltered)); } @Override @@ -252,7 +270,7 @@ public void setOperationPath(String path) { @Override public void setOperationHeaders(MultivaluedMap headers) { - Map headersReduced = new HashMap<>(); + Map headersReduced = new LinkedHashMap<>(); headers.forEach( (key, values) -> { From e2604342c6fecdd75108b8cecc8bf70373f6b65a Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 2 Jun 2026 17:33:44 +0200 Subject: [PATCH 26/44] remove debugging code --- .../base/domain/AuditLogConfiguration.java | 12 ++++++------ .../ii/xtraplatform/services/app/AuditLogImpl.java | 2 -- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java index eb6acf8e..cf295560 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java @@ -38,8 +38,8 @@ default TYPE getType() { } @Default - default HeaderConfiguration getHeaders() { - return ModifiableHeaderConfiguration.create(); + default HeadersConfiguration getHeaders() { + return ModifiableHeadersConfiguration.create(); } enum TYPE { @@ -49,18 +49,18 @@ enum TYPE { @Value.Immutable @Value.Modifiable - @JsonDeserialize(as = ModifiableHeaderConfiguration.class) - interface HeaderConfiguration { + @JsonDeserialize(as = ModifiableHeadersConfiguration.class) + interface HeadersConfiguration { @Value.Default default List getIncluded() { // Find out how to stop default values to merge with custom values // return List.of("*"); - return List.of(""); + return List.of(); } @Value.Default default List getExcluded() { - return List.of(""); + return List.of(); } } } diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index b7da6aca..ef4a6035 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -139,9 +139,7 @@ public void setOperationHeaders(String requestId, MultivaluedMap } List includes = appContext.getConfiguration().getAuditLog().getHeaders().getIncluded(); - LOGGER.error(includes.toString()); List excludes = appContext.getConfiguration().getAuditLog().getHeaders().getExcluded(); - LOGGER.error(excludes.toString()); MultivaluedMap headersFiltered = new MultivaluedHashMap<>(); headers.forEach( From 12b815ac166efb0ab3adceb4e951e380545aa8ad Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Wed, 3 Jun 2026 09:35:25 +0200 Subject: [PATCH 27/44] global config: add getClaims to User --- .../auth/app/JwtTokenHandler.java | 1 + .../de/ii/xtraplatform/auth/domain/User.java | 2 ++ .../base/domain/AuditLogConfiguration.java | 24 ++++++++++++++++++- .../src/main/resources/cfg.base.yml | 6 ++++- .../services/app/AuditLogImpl.java | 4 +--- 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/xtraplatform-auth/src/main/java/de/ii/xtraplatform/auth/app/JwtTokenHandler.java b/xtraplatform-auth/src/main/java/de/ii/xtraplatform/auth/app/JwtTokenHandler.java index 2005b38f..74276ea8 100644 --- a/xtraplatform-auth/src/main/java/de/ii/xtraplatform/auth/app/JwtTokenHandler.java +++ b/xtraplatform-auth/src/main/java/de/ii/xtraplatform/auth/app/JwtTokenHandler.java @@ -295,6 +295,7 @@ public Optional parseToken(String token) { .permissions(readList(claimsJws, claimsProvider.getClaims().getPermissions())) .apiPermissions( readListPerApi(claimsJws, claimsProvider.getClaims().getPermissions())) + .claims(claimsJws) .build(); if (LOGGER.isTraceEnabled()) { diff --git a/xtraplatform-auth/src/main/java/de/ii/xtraplatform/auth/domain/User.java b/xtraplatform-auth/src/main/java/de/ii/xtraplatform/auth/domain/User.java index f90ceb8c..3b0477e5 100644 --- a/xtraplatform-auth/src/main/java/de/ii/xtraplatform/auth/domain/User.java +++ b/xtraplatform-auth/src/main/java/de/ii/xtraplatform/auth/domain/User.java @@ -34,6 +34,8 @@ enum PolicyDecision { Map> getApiPermissions(); + Map getClaims(); + @Value.Default default Role getRole() { return Role.NONE; diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java index cf295560..e5d8c9b6 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java @@ -42,6 +42,11 @@ default HeadersConfiguration getHeaders() { return ModifiableHeadersConfiguration.create(); } + @Default + default ClaimsConfiguration getClaims() { + return ModifiableClaimsConfiguration.create(); + } + enum TYPE { JSON, JSON_PRETTY @@ -53,7 +58,24 @@ enum TYPE { interface HeadersConfiguration { @Value.Default default List getIncluded() { - // Find out how to stop default values to merge with custom values + // ToDo: Find out how to stop default values from merging with custom values + // return List.of("*"); + return List.of(); + } + + @Value.Default + default List getExcluded() { + return List.of(); + } + } + + @Value.Immutable + @Value.Modifiable + @JsonDeserialize(as = ModifiableClaimsConfiguration.class) + interface ClaimsConfiguration { + @Value.Default + default List getIncluded() { + // ToDo: Find out how to stop default values from merging with custom values // return List.of("*"); return List.of(); } diff --git a/xtraplatform-base/src/main/resources/cfg.base.yml b/xtraplatform-base/src/main/resources/cfg.base.yml index b78b0019..760dcd90 100644 --- a/xtraplatform-base/src/main/resources/cfg.base.yml +++ b/xtraplatform-base/src/main/resources/cfg.base.yml @@ -75,8 +75,12 @@ auditLog: retries: 3 type: ${TYPE:-JSON} pathPrefix: '{api}/{date}' + # Find out how to stop default values to merge with custom values headers: - # Find out how to stop default values to merge with custom values + # included: [ '*' ] + included: [ ] + excluded: [ ] + claims: # included: [ '*' ] included: [ ] excluded: [ ] \ No newline at end of file diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index ef4a6035..14e92462 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -144,7 +144,6 @@ public void setOperationHeaders(String requestId, MultivaluedMap headers.forEach( (k, v) -> { - LOGGER.error(k); if (!excludes.contains(k) && !excludes.contains("*") && (includes.contains("*") || includes.contains(k))) { @@ -199,9 +198,8 @@ public boolean writeAndRemoveLog(String requestId) { return false; } - int maxRetries = appContext.getConfiguration().getAuditLog().getRetries(); Path path = createPath(requestId, log); - + int maxRetries = appContext.getConfiguration().getAuditLog().getRetries(); int retries = 0; do { try { From db41d5c61fd625b19ccb466abbbf484d40775199 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Wed, 3 Jun 2026 09:58:49 +0200 Subject: [PATCH 28/44] global config: claims (without default values) --- .../services/app/AuditLogImpl.java | 30 +++++++++++++++---- .../services/domain/AuditLog.java | 6 ++-- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 14e92462..f5ead8af 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -92,6 +92,23 @@ private Path createPath(String requestId, Log log) { return Path.of(pathPrefix).resolve(Path.of(requestId + ".json")); } + private Map filterClaims(Map claims) { + Map filteredClaims = new LinkedHashMap<>(); + List includes = appContext.getConfiguration().getAuditLog().getClaims().getIncluded(); + List excludes = appContext.getConfiguration().getAuditLog().getClaims().getExcluded(); + + claims.forEach( + (k, v) -> { + if (!excludes.contains(k) + && !excludes.contains("*") + && (includes.contains("*") || includes.contains(k))) { + filteredClaims.put(k, v); + } + }); + + return filteredClaims; + } + @Override public void createLog(String requestId) { if (isDisabled()) { @@ -109,11 +126,13 @@ public void setApi(String requestId, String api) { } @Override - public void setActor(String requestId, String actorType, String actorId) { + public void setActor( + String requestId, String actorType, String actorId, Map claims) { if (isDisabled()) { return; } - getOptionalLog(requestId).ifPresent(log -> log.setActor(actorType, actorId)); + getOptionalLog(requestId) + .ifPresent(log -> log.setActor(actorType, actorId, filterClaims(claims))); } @Override @@ -227,7 +246,7 @@ public boolean writeAndRemoveLog(String requestId) { public static class LogImpl implements Log { private final String id; private final Instant started; - private final Map actor = new LinkedHashMap<>(); + private final Map actor = new LinkedHashMap<>(); private final Map operation = new LinkedHashMap<>(); private Instant finished; private Map target; @@ -249,9 +268,10 @@ public void setApi(String api) { } @Override - public void setActor(String actorType, String actorId) { + public void setActor(String actorType, String actorId, Map claims) { actor.put("type", actorType); actor.put("id", actorId); + actor.put("claims", claims); } @Override @@ -314,7 +334,7 @@ public String getApi() { @JsonProperty("actor") @Override - public Map getActor() { + public Map getActor() { return actor; } diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java index 9c55d387..3582311e 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java @@ -16,7 +16,7 @@ public interface AuditLog { void setApi(String requestId, String api); - void setActor(String requestId, String actorType, String actorId); + void setActor(String requestId, String actorType, String actorId, Map claims); void setOperationMethod(String requestId, String method); @@ -36,7 +36,7 @@ interface Log { void setApi(String api); - void setActor(String actorType, String actorId); + void setActor(String actorType, String actorId, Map claims); void setOperationMethod(String method); @@ -56,7 +56,7 @@ interface Log { String getApi(); - Map getActor(); + Map getActor(); Map getOperation(); From 5c17543c0791366ac1307fc4ad9b603cc63037f3 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Wed, 3 Jun 2026 14:18:26 +0200 Subject: [PATCH 29/44] global config: httpStatus (without default values) --- .../base/domain/AuditLogConfiguration.java | 22 +++++++++++++++++++ .../src/main/resources/cfg.base.yml | 4 ++++ .../services/app/AuditLogImpl.java | 20 ++++++++--------- .../services/app/AuditLogResponseFilter.java | 16 +++++++++++--- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java index e5d8c9b6..2a638fbe 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java @@ -47,6 +47,11 @@ default ClaimsConfiguration getClaims() { return ModifiableClaimsConfiguration.create(); } + @Default + default HttpStatusConfiguration getHttpStatus() { + return ModifiableHttpStatusConfiguration.create(); + } + enum TYPE { JSON, JSON_PRETTY @@ -85,4 +90,21 @@ default List getExcluded() { return List.of(); } } + + @Value.Immutable + @Value.Modifiable + @JsonDeserialize(as = ModifiableHttpStatusConfiguration.class) + interface HttpStatusConfiguration { + @Value.Default + default List getIncluded() { + // ToDo: Find out how to stop default values from merging with custom values + // return List.of("200"); + return List.of(); + } + + @Value.Default + default List getExcluded() { + return List.of(); + } + } } diff --git a/xtraplatform-base/src/main/resources/cfg.base.yml b/xtraplatform-base/src/main/resources/cfg.base.yml index 760dcd90..b981ef5d 100644 --- a/xtraplatform-base/src/main/resources/cfg.base.yml +++ b/xtraplatform-base/src/main/resources/cfg.base.yml @@ -83,4 +83,8 @@ auditLog: claims: # included: [ '*' ] included: [ ] + excluded: [ ] + httpStatus: + # included: [ '200' ] + included: [ ] excluded: [ ] \ No newline at end of file diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index f5ead8af..97611fa6 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -94,14 +94,14 @@ private Path createPath(String requestId, Log log) { private Map filterClaims(Map claims) { Map filteredClaims = new LinkedHashMap<>(); - List includes = appContext.getConfiguration().getAuditLog().getClaims().getIncluded(); - List excludes = appContext.getConfiguration().getAuditLog().getClaims().getExcluded(); + List included = appContext.getConfiguration().getAuditLog().getClaims().getIncluded(); + List excluded = appContext.getConfiguration().getAuditLog().getClaims().getExcluded(); claims.forEach( (k, v) -> { - if (!excludes.contains(k) - && !excludes.contains("*") - && (includes.contains("*") || includes.contains(k))) { + if (!excluded.contains("*") + && !excluded.contains(k) + && (included.contains("*") || included.contains(k))) { filteredClaims.put(k, v); } }); @@ -157,15 +157,15 @@ public void setOperationHeaders(String requestId, MultivaluedMap return; } - List includes = appContext.getConfiguration().getAuditLog().getHeaders().getIncluded(); - List excludes = appContext.getConfiguration().getAuditLog().getHeaders().getExcluded(); + List included = appContext.getConfiguration().getAuditLog().getHeaders().getIncluded(); + List excluded = appContext.getConfiguration().getAuditLog().getHeaders().getExcluded(); MultivaluedMap headersFiltered = new MultivaluedHashMap<>(); headers.forEach( (k, v) -> { - if (!excludes.contains(k) - && !excludes.contains("*") - && (includes.contains("*") || includes.contains(k))) { + if (!excluded.contains("*") + && !excluded.contains(k) + && (included.contains("*") || included.contains(k))) { headersFiltered.addAll(k, v); } }); diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java index 9aa24fb3..2046755e 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java @@ -16,6 +16,7 @@ import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; import java.io.IOException; +import java.util.List; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,8 +52,17 @@ public void filter( } String requestId = requestIdObject.toString(); - auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); - // ToDo: Abort response if false is returned! - auditLog.writeAndRemoveLog(requestId); + String statusCode = String.valueOf(responseContext.getStatus()); + List included = + appContext.getConfiguration().getAuditLog().getHttpStatus().getIncluded(); + List excluded = + appContext.getConfiguration().getAuditLog().getHttpStatus().getExcluded(); + if (!excluded.contains("*") + && !excluded.contains(statusCode) + && (included.contains("*") || included.contains(statusCode))) { + auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); + // ToDo: Abort response if false is returned! + auditLog.writeAndRemoveLog(requestId); + } } } From ec5b42a29a65b6c2020ea8ba53352fee5020f363 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Wed, 3 Jun 2026 15:00:35 +0200 Subject: [PATCH 30/44] abort response on logging-error --- .../services/app/AuditLogResponseFilter.java | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java index 2046755e..3cc4060e 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java @@ -28,6 +28,18 @@ public class AuditLogResponseFilter implements ContainerResponseFilter { private final AuditLog auditLog; private final AppContext appContext; + private boolean sufficientHttpCode(int statusCodeInt) { + String statusCode = String.valueOf(statusCodeInt); + List included = + appContext.getConfiguration().getAuditLog().getHttpStatus().getIncluded(); + List excluded = + appContext.getConfiguration().getAuditLog().getHttpStatus().getExcluded(); + + return !excluded.contains("*") + && !excluded.contains(statusCode) + && (included.contains("*") || included.contains(statusCode)); + } + @Inject public AuditLogResponseFilter(AuditLog auditLog, AppContext appContext) { this.auditLog = auditLog; @@ -52,17 +64,15 @@ public void filter( } String requestId = requestIdObject.toString(); - String statusCode = String.valueOf(responseContext.getStatus()); - List included = - appContext.getConfiguration().getAuditLog().getHttpStatus().getIncluded(); - List excluded = - appContext.getConfiguration().getAuditLog().getHttpStatus().getExcluded(); - if (!excluded.contains("*") - && !excluded.contains(statusCode) - && (included.contains("*") || included.contains(statusCode))) { + if (sufficientHttpCode(responseContext.getStatus())) { auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); - // ToDo: Abort response if false is returned! - auditLog.writeAndRemoveLog(requestId); + boolean logSuccessful = auditLog.writeAndRemoveLog(requestId); + if (!logSuccessful) { + // ToDo: Evaluate if there is a better way to abort the response + responseContext.setStatus(500); + responseContext.setEntity(null); + responseContext.getHeaders().clear(); + } } } } From 94e09afcc2662c088b4342c6cb890ba099241fe8 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Wed, 3 Jun 2026 16:50:22 +0200 Subject: [PATCH 31/44] global config: add docs --- .../base/domain/AppConfiguration.java | 4 + .../base/domain/AuditLogConfiguration.java | 91 ++++++++++++++++++- 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AppConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AppConfiguration.java index 7fa2af08..26f8096b 100755 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AppConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AppConfiguration.java @@ -151,6 +151,10 @@ && getJobs().getMaxConcurrent() == 1) { @Valid public abstract RedisConfiguration getRedis(); + /** + * @langEn See [AuditLog](120-auditLog.md). + * @langDe Siehe [AuditLog](120-auditLog.md). + */ @JsonProperty("auditLog") @Valid public abstract AuditLogConfiguration getAuditLog(); diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java index 2a638fbe..f2519a82 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java @@ -8,45 +8,131 @@ package de.ii.xtraplatform.base.domain; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import de.ii.xtraplatform.docs.DocFile; +import de.ii.xtraplatform.docs.DocStep; +import de.ii.xtraplatform.docs.DocStep.Step; +import de.ii.xtraplatform.docs.DocTable; +import de.ii.xtraplatform.docs.DocTable.ColumnSet; import java.util.List; import org.immutables.value.Value; import org.immutables.value.Value.Default; +/** + * @langEn # AuditLog + *

## Options + *

{@docTable:properties} + * @langDe # AuditLog + *

## Optionen + *

{@docTable:properties} + * @ref:cfgProperties {@link de.ii.xtraplatform.base.domain.ImmutableAuditLogConfiguration} + */ +@DocFile( + path = "application/20-configuration", + name = "120-auditLog.md", + tables = { + @DocTable( + name = "properties", + rows = { + @DocStep(type = Step.TAG_REFS, params = "{@ref:cfgProperties}"), + @DocStep(type = Step.JSON_PROPERTIES) + }, + columnSet = ColumnSet.JSON_PROPERTIES) + }) @Value.Immutable @Value.Modifiable @JsonDeserialize(as = ModifiableAuditLogConfiguration.class) public interface AuditLogConfiguration { + /** + * @langEn If `true`, audit logging is enabled; otherwise, it is disabled. + * @langDe Wenn `true`, wird das Audit-Logging eingeschaltet, ansonsten deaktiviert. + * @default false + */ @Default default boolean getEnabled() { return false; } + /** + * @langEn Indicates how often the write process should be retried on errors. Should be set to 0 + * if no retries are desired. + * @langDe Gibt an, wie oft der Schreibprozess bei Fehlern wiederholt werden soll. Sollte auf 0 + * gesetzt werden, falls keine Neuversuche erwünscht sind. + * @default 3 + */ @Default default int getRetries() { return 3; } + /** + * @langEn Specifies the path to prepend to the log file. `{api}` and `{date}` are replaced with + * the API ID and the request's ISO date, respectively. If the request is API-independent, + * `{api}` is replaced with "homepage". For example, log files for `{api}/foo/{date}/bar` + * could be stored at `resources/logs/audit/vineyards/foo/2026-06-03/bar`. + * @langDe Gibt den Pfad an, der vor der Log-Datei angehängt werden soll. Dabei werden `{api}` und + * `{date}` jeweils mit der API-ID bzw. dem ISO-Datum der Anfrage ersetzt. Falls die Anfrage + * API-unabhängig ist, wird `{api}` mit "homepage" ersetzt. Beispielsweise könnten die + * Log-Dateien für `{api}/foo/{date}/bar` unter + * `resources/logs/audit/vineyards/foo/2026-06-03/bar` gespeichert werden. + * @default {api}/{date} + */ @Default default String getPathPrefix() { return "{api}/{date}"; } + /** + * @langEn Specifies the format in which logs are stored. Currently supported: `JSON` and + * `JSON_PRETTY` (formatted JSON). + * @langDe Gibt an, in welchem Format die Logs gespeichert werden. Unterstützt werden momentan + * `JSON` und `JSON_PRETTY` (formatiertes JSON). + * @default JSON + */ @Default default TYPE getType() { return TYPE.JSON; } + /** + * @langEn The `included` list specifies which headers should be logged. The `excluded` list + * specifies which headers would be logged according to `included` but should not be logged. + * The special value `*` can be used for both lists and covers all headers. If `excluded = [ + * '*' ]`, no headers are logged. + * @langDe Die `included`-Liste gibt an, welche Header geloggt werden sollen. Die `excluded`-Liste + * gibt an, welche Header gemäß `included` geloggt würden, aber nicht geloggt werden sollen. + * Der spezielle Wert `*` kann für beide Listen verwendet werden und umfasst alle Header. Wenn + * `excluded = [ '*' ]`, werden keine Header geloggt. + * @default included: []\nexcluded: [] + */ @Default default HeadersConfiguration getHeaders() { return ModifiableHeadersConfiguration.create(); } + /** + * @langEn Specifies which claims from the token should be logged and which should explicitly not + * be logged by using `included`/`excluded` lists. The exact logic is the same as in + * `headers`. + * @langDe Gibt mit `included`/`excluded`-Listen an, welche Claims aus dem Token geloggt werden + * sollen bzw. explizit nicht geloggt werden dürfen. Die genaue Logik entspricht der in + * `headers`. + * @default included: []\nexcluded: [] + */ @Default default ClaimsConfiguration getClaims() { return ModifiableClaimsConfiguration.create(); } + /** + * @langEn Specifies for which HTTP status codes requests should be logged and which should + * explicitly not be logged by using `included`/`excluded` lists. The exact logic is the same + * as in `headers`. + * @langDe Gibt mit `included`/`excluded`-Listen an, bei welchen HTTP-Status-Codes Anfragen + * geloggt werden sollen bzw. explizit nicht geloggt werden dürfen. Die genaue Logik + * entspricht der in `headers`. + * @default included: []\nexcluded: [] + */ @Default default HttpStatusConfiguration getHttpStatus() { return ModifiableHttpStatusConfiguration.create(); @@ -57,13 +143,14 @@ enum TYPE { JSON_PRETTY } + // ToDo: Find out how to stop default values from merging with custom values + @Value.Immutable @Value.Modifiable @JsonDeserialize(as = ModifiableHeadersConfiguration.class) interface HeadersConfiguration { @Value.Default default List getIncluded() { - // ToDo: Find out how to stop default values from merging with custom values // return List.of("*"); return List.of(); } @@ -80,7 +167,6 @@ default List getExcluded() { interface ClaimsConfiguration { @Value.Default default List getIncluded() { - // ToDo: Find out how to stop default values from merging with custom values // return List.of("*"); return List.of(); } @@ -97,7 +183,6 @@ default List getExcluded() { interface HttpStatusConfiguration { @Value.Default default List getIncluded() { - // ToDo: Find out how to stop default values from merging with custom values // return List.of("200"); return List.of(); } From 0017639bdb188c4f32bf880f1c33b1129b099f54 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Fri, 5 Jun 2026 14:49:30 +0200 Subject: [PATCH 32/44] global config: fix merging of default lists --- .../base/domain/AuditLogConfiguration.java | 21 ++++++++++++------- .../src/main/resources/cfg.base.yml | 8 ++----- .../services/app/AuditLogImpl.java | 6 ++++++ .../services/app/AuditLogResponseFilter.java | 2 +- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java index f2519a82..731b02f1 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java @@ -7,6 +7,8 @@ */ package de.ii.xtraplatform.base.domain; +import com.fasterxml.jackson.annotation.JsonMerge; +import com.fasterxml.jackson.annotation.OptBoolean; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import de.ii.xtraplatform.docs.DocFile; import de.ii.xtraplatform.docs.DocStep; @@ -103,7 +105,7 @@ default TYPE getType() { * gibt an, welche Header gemäß `included` geloggt würden, aber nicht geloggt werden sollen. * Der spezielle Wert `*` kann für beide Listen verwendet werden und umfasst alle Header. Wenn * `excluded = [ '*' ]`, werden keine Header geloggt. - * @default included: []\nexcluded: [] + * @default included: [ '*' ]\nexcluded: [] */ @Default default HeadersConfiguration getHeaders() { @@ -117,7 +119,7 @@ default HeadersConfiguration getHeaders() { * @langDe Gibt mit `included`/`excluded`-Listen an, welche Claims aus dem Token geloggt werden * sollen bzw. explizit nicht geloggt werden dürfen. Die genaue Logik entspricht der in * `headers`. - * @default included: []\nexcluded: [] + * @default included: [ '*' ]\nexcluded: [] */ @Default default ClaimsConfiguration getClaims() { @@ -131,7 +133,7 @@ default ClaimsConfiguration getClaims() { * @langDe Gibt mit `included`/`excluded`-Listen an, bei welchen HTTP-Status-Codes Anfragen * geloggt werden sollen bzw. explizit nicht geloggt werden dürfen. Die genaue Logik * entspricht der in `headers`. - * @default included: []\nexcluded: [] + * @default included: [ '200' ]\nexcluded: [] */ @Default default HttpStatusConfiguration getHttpStatus() { @@ -150,12 +152,13 @@ enum TYPE { @JsonDeserialize(as = ModifiableHeadersConfiguration.class) interface HeadersConfiguration { @Value.Default + @JsonMerge(OptBoolean.FALSE) default List getIncluded() { - // return List.of("*"); - return List.of(); + return List.of("*"); } @Value.Default + @JsonMerge(OptBoolean.FALSE) default List getExcluded() { return List.of(); } @@ -166,12 +169,13 @@ default List getExcluded() { @JsonDeserialize(as = ModifiableClaimsConfiguration.class) interface ClaimsConfiguration { @Value.Default + @JsonMerge(OptBoolean.FALSE) default List getIncluded() { - // return List.of("*"); return List.of(); } @Value.Default + @JsonMerge(OptBoolean.FALSE) default List getExcluded() { return List.of(); } @@ -182,12 +186,13 @@ default List getExcluded() { @JsonDeserialize(as = ModifiableHttpStatusConfiguration.class) interface HttpStatusConfiguration { @Value.Default + @JsonMerge(OptBoolean.FALSE) default List getIncluded() { - // return List.of("200"); - return List.of(); + return List.of("200"); } @Value.Default + @JsonMerge(OptBoolean.FALSE) default List getExcluded() { return List.of(); } diff --git a/xtraplatform-base/src/main/resources/cfg.base.yml b/xtraplatform-base/src/main/resources/cfg.base.yml index b981ef5d..2f5929f7 100644 --- a/xtraplatform-base/src/main/resources/cfg.base.yml +++ b/xtraplatform-base/src/main/resources/cfg.base.yml @@ -75,16 +75,12 @@ auditLog: retries: 3 type: ${TYPE:-JSON} pathPrefix: '{api}/{date}' - # Find out how to stop default values to merge with custom values headers: - # included: [ '*' ] - included: [ ] + included: [ '*' ] excluded: [ ] claims: - # included: [ '*' ] included: [ ] excluded: [ ] httpStatus: - # included: [ '200' ] - included: [ ] + included: [ '200' ] excluded: [ ] \ No newline at end of file diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 97611fa6..1e793f04 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -158,7 +158,13 @@ public void setOperationHeaders(String requestId, MultivaluedMap } List included = appContext.getConfiguration().getAuditLog().getHeaders().getIncluded(); + if (LOGGER.isErrorEnabled()) { + LOGGER.error(included.toString()); + } List excluded = appContext.getConfiguration().getAuditLog().getHeaders().getExcluded(); + if (LOGGER.isErrorEnabled()) { + LOGGER.error(excluded.toString()); + } MultivaluedMap headersFiltered = new MultivaluedHashMap<>(); headers.forEach( diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java index 3cc4060e..e66fa6a9 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java @@ -68,7 +68,7 @@ public void filter( auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); boolean logSuccessful = auditLog.writeAndRemoveLog(requestId); if (!logSuccessful) { - // ToDo: Evaluate if there is a better way to abort the response + // ToDo: Evaluate if there is a better way to abort the response (Fehler werfen) responseContext.setStatus(500); responseContext.setEntity(null); responseContext.getHeaders().clear(); From e463ec1bb5ad6f11663c9e7809c49c94e4cbbe20 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Fri, 5 Jun 2026 14:54:46 +0200 Subject: [PATCH 33/44] remove debugging code --- .../services/app/AuditLogImpl.java | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 1e793f04..d648d8aa 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -41,10 +41,10 @@ @AutoBind public class AuditLogImpl implements AuditLog { private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogImpl.class); - private ObjectWriter objectWriter; private final Map auditLogMapping = new ConcurrentHashMap<>(); private final ResourceStore auditLogStore; private final AppContext appContext; + private ObjectWriter objectWriter; @Inject AuditLogImpl(Jackson jackson, ResourceStore resourceStore, AppContext appContext) { @@ -158,13 +158,7 @@ public void setOperationHeaders(String requestId, MultivaluedMap } List included = appContext.getConfiguration().getAuditLog().getHeaders().getIncluded(); - if (LOGGER.isErrorEnabled()) { - LOGGER.error(included.toString()); - } List excluded = appContext.getConfiguration().getAuditLog().getHeaders().getExcluded(); - if (LOGGER.isErrorEnabled()) { - LOGGER.error(excluded.toString()); - } MultivaluedMap headersFiltered = new MultivaluedHashMap<>(); headers.forEach( @@ -268,11 +262,6 @@ public void finish() { finished = Instant.now(); } - @Override - public void setApi(String api) { - this.api = api; - } - @Override public void setActor(String actorType, String actorId, Map claims) { actor.put("type", actorType); @@ -309,11 +298,6 @@ public void setOperationStatus(String status) { operation.put("status", status); } - @Override - public void setTarget(Map target) { - this.target = new LinkedHashMap<>(target); - } - @JsonProperty("id") @Override public String getId() { @@ -338,6 +322,11 @@ public String getApi() { return api; } + @Override + public void setApi(String api) { + this.api = api; + } + @JsonProperty("actor") @Override public Map getActor() { @@ -355,5 +344,10 @@ public Map getOperation() { public Map getTarget() { return target; } + + @Override + public void setTarget(Map target) { + this.target = new LinkedHashMap<>(target); + } } } From 4c1f0d7b096fd28e5ff1c1194c7d282ed4722e5e Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Fri, 5 Jun 2026 15:00:10 +0200 Subject: [PATCH 34/44] global config: throw error on IO-Exception --- .../ii/xtraplatform/services/app/AuditLogResponseFilter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java index e66fa6a9..971dd1e5 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java @@ -12,6 +12,7 @@ import de.ii.xtraplatform.services.domain.AuditLog; import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.ws.rs.ServerErrorException; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; @@ -68,10 +69,10 @@ public void filter( auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); boolean logSuccessful = auditLog.writeAndRemoveLog(requestId); if (!logSuccessful) { - // ToDo: Evaluate if there is a better way to abort the response (Fehler werfen) responseContext.setStatus(500); responseContext.setEntity(null); responseContext.getHeaders().clear(); + throw new ServerErrorException("Internal Server Error", 500); } } } From 2a898daf296e70bd655a4fd0e41f6c0ca28a5963 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Fri, 5 Jun 2026 15:39:25 +0200 Subject: [PATCH 35/44] AuditLog: extend by logIsAvaible and isEnabled --- .../ii/xtraplatform/services/app/AuditLogImpl.java | 12 +++++++++++- .../de/ii/xtraplatform/services/domain/AuditLog.java | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index d648d8aa..d76a6ec8 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -74,7 +74,7 @@ private Optional getOptionalLog(String requestId) { } private boolean isDisabled() { - return !appContext.getConfiguration().getAuditLog().getEnabled(); + return !isEnabled(); } private Path createPath(String requestId, Log log) { @@ -117,6 +117,16 @@ public void createLog(String requestId) { auditLogMapping.computeIfAbsent(requestId, k -> new LogImpl(requestId)); } + @Override + public boolean logIsAvaible(String requestId) { + return !isDisabled() && auditLogMapping.containsKey(requestId); + } + + @Override + public boolean isEnabled() { + return appContext.getConfiguration().getAuditLog().getEnabled(); + } + @Override public void setApi(String requestId, String api) { if (isDisabled()) { diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java index 3582311e..3272f5fb 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java @@ -14,6 +14,10 @@ public interface AuditLog { void createLog(String requestId); + boolean logIsAvaible(String requestId); + + boolean isEnabled(); + void setApi(String requestId, String api); void setActor(String requestId, String actorType, String actorId, Map claims); From f16484dd51f8ef1de950e70043acb26f54089063 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Mon, 8 Jun 2026 12:28:38 +0200 Subject: [PATCH 36/44] AuditLog: add abort method --- .../services/app/AuditLogImpl.java | 9 +++- .../services/app/AuditLogResponseFilter.java | 47 ++++++++++++------- .../services/domain/AuditLog.java | 14 +++--- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index d76a6ec8..3b784571 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -118,7 +118,12 @@ public void createLog(String requestId) { } @Override - public boolean logIsAvaible(String requestId) { + public void abortLog(String requestId) { + auditLogMapping.remove(requestId); + } + + @Override + public boolean logIsAvailable(String requestId) { return !isDisabled() && auditLogMapping.containsKey(requestId); } @@ -201,7 +206,7 @@ public void setTarget(String requestId, Map target) { @Override @SuppressWarnings({"PMD.CognitiveComplexity", "PMD.CyclomaticComplexity"}) - public boolean writeAndRemoveLog(String requestId) { + public boolean removeAndWriteLog(String requestId) { if (isDisabled()) { return false; } diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java index 971dd1e5..8e09d97c 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java @@ -29,6 +29,12 @@ public class AuditLogResponseFilter implements ContainerResponseFilter { private final AuditLog auditLog; private final AppContext appContext; + @Inject + public AuditLogResponseFilter(AuditLog auditLog, AppContext appContext) { + this.auditLog = auditLog; + this.appContext = appContext; + } + private boolean sufficientHttpCode(int statusCodeInt) { String statusCode = String.valueOf(statusCodeInt); List included = @@ -41,39 +47,46 @@ private boolean sufficientHttpCode(int statusCodeInt) { && (included.contains("*") || included.contains(statusCode)); } - @Inject - public AuditLogResponseFilter(AuditLog auditLog, AppContext appContext) { - this.auditLog = auditLog; - this.appContext = appContext; - } - @Override public void filter( ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { + // Return if auditLog is disabled in global config (cfg.yml) if (!appContext.getConfiguration().getAuditLog().getEnabled()) { return; } + // Return if any of the context objects are missing if (Objects.isNull(requestContext) || Objects.isNull(responseContext)) { return; } + // Return if the requestId is missing Object requestIdObject = requestContext.getProperty("REQUEST_ID"); - if (Objects.isNull(requestIdObject)) { + if (!(requestIdObject instanceof String requestId)) { + return; + } + + // Return if no log is available (for example if the log was aborted during another step) + if (!auditLog.logIsAvailable(requestId)) { + return; + } + + // Abort log and return if HTTP-Code is not applicable according to the global config + if (!sufficientHttpCode(responseContext.getStatus())) { + auditLog.abortLog(requestId); return; } - String requestId = requestIdObject.toString(); - if (sufficientHttpCode(responseContext.getStatus())) { - auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); - boolean logSuccessful = auditLog.writeAndRemoveLog(requestId); - if (!logSuccessful) { - responseContext.setStatus(500); - responseContext.setEntity(null); - responseContext.getHeaders().clear(); - throw new ServerErrorException("Internal Server Error", 500); - } + // Log status and write log + auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); + boolean logSuccessful = auditLog.removeAndWriteLog(requestId); + // Abort request if writing the log was not successful! + if (!logSuccessful) { + responseContext.setStatus(500); + responseContext.setEntity(null); + responseContext.getHeaders().clear(); + throw new ServerErrorException("Internal Server Error", 500); } } } diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java index 3272f5fb..7444a8fc 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java @@ -14,10 +14,12 @@ public interface AuditLog { void createLog(String requestId); - boolean logIsAvaible(String requestId); + void abortLog(String requestId); boolean isEnabled(); + boolean logIsAvailable(String requestId); + void setApi(String requestId, String api); void setActor(String requestId, String actorType, String actorId, Map claims); @@ -32,14 +34,12 @@ public interface AuditLog { void setTarget(String requestId, Map target); - boolean writeAndRemoveLog(String requestId); + boolean removeAndWriteLog(String requestId); interface Log { void finish(); - void setApi(String api); - void setActor(String actorType, String actorId, Map claims); void setOperationMethod(String method); @@ -50,8 +50,6 @@ interface Log { void setOperationStatus(String status); - void setTarget(Map target); - String getId(); Instant getStarted(); @@ -60,10 +58,14 @@ interface Log { String getApi(); + void setApi(String api); + Map getActor(); Map getOperation(); Map getTarget(); + + void setTarget(Map target); } } From 5da7593957b247613c5a8c1a0f08deb8375aba57 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 9 Jun 2026 12:42:13 +0200 Subject: [PATCH 37/44] refactor --- .../services/app/AuditLogImpl.java | 8 +++----- .../services/app/AuditLogResponseFilter.java | 20 ++++++++++++------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 3b784571..90b3f0f1 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -205,7 +205,7 @@ public void setTarget(String requestId, Map target) { } @Override - @SuppressWarnings({"PMD.CognitiveComplexity", "PMD.CyclomaticComplexity"}) + @SuppressWarnings({"PMD.CognitiveComplexity"}) public boolean removeAndWriteLog(String requestId) { if (isDisabled()) { return false; @@ -213,9 +213,7 @@ public boolean removeAndWriteLog(String requestId) { Log log = auditLogMapping.remove(requestId); if (Objects.isNull(log)) { - if (LOGGER.isErrorEnabled()) { - LOGGER.error("No AuditLog-object found for requestId {}", requestId); - } + LOGGER.error("No AuditLog-object found for requestId {}", requestId); return false; } @@ -228,7 +226,7 @@ public boolean removeAndWriteLog(String requestId) { } inputStream = new ByteArrayInputStream(objectWriter.writeValueAsBytes(log)); } catch (JsonProcessingException e) { - LOGGER.error("Failed to serialize log " + requestId, e); + LOGGER.error("Failed to serialize log {}", requestId, e); return false; } diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java index 8e09d97c..9d03b06e 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java @@ -19,13 +19,10 @@ import java.io.IOException; import java.util.List; import java.util.Objects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Singleton @AutoBind public class AuditLogResponseFilter implements ContainerResponseFilter { - private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogResponseFilter.class); private final AuditLog auditLog; private final AppContext appContext; @@ -52,12 +49,12 @@ public void filter( ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { // Return if auditLog is disabled in global config (cfg.yml) - if (!appContext.getConfiguration().getAuditLog().getEnabled()) { + if (!auditLog.isEnabled()) { return; } - // Return if any of the context objects are missing - if (Objects.isNull(requestContext) || Objects.isNull(responseContext)) { + // Return if the request context is missing + if (Objects.isNull(requestContext)) { return; } @@ -72,15 +69,24 @@ public void filter( return; } + // Abort log and return if the response context is missing + if (Objects.isNull(responseContext)) { + auditLog.abortLog(requestId); + return; + } + // Abort log and return if HTTP-Code is not applicable according to the global config if (!sufficientHttpCode(responseContext.getStatus())) { auditLog.abortLog(requestId); return; } - // Log status and write log + // Log status auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); + + // Write the final log and save result boolean logSuccessful = auditLog.removeAndWriteLog(requestId); + // Abort request if writing the log was not successful! if (!logSuccessful) { responseContext.setStatus(500); From 361203f83a861efc644f5b444d8f763470e2ed1e Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 9 Jun 2026 13:08:27 +0200 Subject: [PATCH 38/44] landingpage instead of homepage on missing api --- .../de/ii/xtraplatform/base/domain/AuditLogConfiguration.java | 4 ++-- .../java/de/ii/xtraplatform/services/app/AuditLogImpl.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java index 731b02f1..a16870cf 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java @@ -70,11 +70,11 @@ default int getRetries() { /** * @langEn Specifies the path to prepend to the log file. `{api}` and `{date}` are replaced with * the API ID and the request's ISO date, respectively. If the request is API-independent, - * `{api}` is replaced with "homepage". For example, log files for `{api}/foo/{date}/bar` + * `{api}` is replaced with "landingpage". For example, log files for `{api}/foo/{date}/bar` * could be stored at `resources/logs/audit/vineyards/foo/2026-06-03/bar`. * @langDe Gibt den Pfad an, der vor der Log-Datei angehängt werden soll. Dabei werden `{api}` und * `{date}` jeweils mit der API-ID bzw. dem ISO-Datum der Anfrage ersetzt. Falls die Anfrage - * API-unabhängig ist, wird `{api}` mit "homepage" ersetzt. Beispielsweise könnten die + * API-unabhängig ist, wird `{api}` mit "landingpage" ersetzt. Beispielsweise könnten die * Log-Dateien für `{api}/foo/{date}/bar` unter * `resources/logs/audit/vineyards/foo/2026-06-03/bar` gespeichert werden. * @default {api}/{date} diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 90b3f0f1..07151fab 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -81,7 +81,7 @@ private Path createPath(String requestId, Log log) { String pathPrefix = appContext.getConfiguration().getAuditLog().getPathPrefix(); // Replace api - String api = Objects.isNull(log.getApi()) ? "homepage" : log.getApi(); + String api = Objects.isNull(log.getApi()) ? "landingpage" : log.getApi(); pathPrefix = pathPrefix.replace("{api}", api); // Replace date From 7f46a1088bcd2a498d5d900d96ae8f23ceaf6700 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 9 Jun 2026 14:33:06 +0200 Subject: [PATCH 39/44] Set anonymous user automatically if actor is missing --- .../java/de/ii/xtraplatform/services/app/AuditLogImpl.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 07151fab..e593f446 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -343,6 +343,12 @@ public void setApi(String api) { @JsonProperty("actor") @Override public Map getActor() { + // Apply the anonymous user if no actor has been set + if (!actor.containsKey("type") && !actor.containsKey("id")) { + actor.put("type", "AnonymousUser"); + actor.put("id", "Anonymous"); + } + return actor; } From 920f5ef2b438b2af03f73cb896d3653fdf59ee19 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Tue, 9 Jun 2026 15:59:46 +0200 Subject: [PATCH 40/44] wait for entity --- .../services/app/AuditLogResponseFilter.java | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java index 9d03b06e..b738cbe5 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java @@ -16,6 +16,8 @@ import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.core.StreamingOutput; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import java.util.Objects; @@ -25,25 +27,39 @@ public class AuditLogResponseFilter implements ContainerResponseFilter { private final AuditLog auditLog; private final AppContext appContext; + private final List included; + private final List excluded; @Inject public AuditLogResponseFilter(AuditLog auditLog, AppContext appContext) { this.auditLog = auditLog; this.appContext = appContext; + included = appContext.getConfiguration().getAuditLog().getHttpStatus().getIncluded(); + excluded = appContext.getConfiguration().getAuditLog().getHttpStatus().getExcluded(); } private boolean sufficientHttpCode(int statusCodeInt) { String statusCode = String.valueOf(statusCodeInt); - List included = - appContext.getConfiguration().getAuditLog().getHttpStatus().getIncluded(); - List excluded = - appContext.getConfiguration().getAuditLog().getHttpStatus().getExcluded(); return !excluded.contains("*") && !excluded.contains(statusCode) && (included.contains("*") || included.contains(statusCode)); } + private void waitForEntity(ContainerResponseContext responseContext) throws IOException { + if (responseContext.getEntity() instanceof StreamingOutput streamingOutput) { + // Block until stream is finished + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + streamingOutput.write(baos); + + // Manually set entity + byte[] buffered = baos.toByteArray(); + responseContext.setEntity(buffered); + responseContext.getHeaders().putSingle("Content-Length", buffered.length); + } + } + + @SuppressWarnings("PMD.CyclomaticComplexity") @Override public void filter( ContainerRequestContext requestContext, ContainerResponseContext responseContext) @@ -59,8 +75,7 @@ public void filter( } // Return if the requestId is missing - Object requestIdObject = requestContext.getProperty("REQUEST_ID"); - if (!(requestIdObject instanceof String requestId)) { + if (!(requestContext.getProperty("REQUEST_ID") instanceof String requestId)) { return; } @@ -75,6 +90,16 @@ public void filter( return; } + try { + waitForEntity(responseContext); + } catch (IOException e) { + auditLog.abortLog(requestId); + responseContext.setStatus(500); + responseContext.setEntity(null); + responseContext.getHeaders().clear(); + throw new ServerErrorException("Internal Server Error", 500, e); + } + // Abort log and return if HTTP-Code is not applicable according to the global config if (!sufficientHttpCode(responseContext.getStatus())) { auditLog.abortLog(requestId); From ae80fea6aecd05066b0d619e52880037cf29fc16 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Wed, 10 Jun 2026 12:04:23 +0200 Subject: [PATCH 41/44] Refactor --- .../de/ii/xtraplatform/base/domain/AuditLogConfiguration.java | 2 -- .../ii/xtraplatform/services/app/AuditLogResponseFilter.java | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java index a16870cf..3f0748d1 100644 --- a/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java +++ b/xtraplatform-base/src/main/java/de/ii/xtraplatform/base/domain/AuditLogConfiguration.java @@ -145,8 +145,6 @@ enum TYPE { JSON_PRETTY } - // ToDo: Find out how to stop default values from merging with custom values - @Value.Immutable @Value.Modifiable @JsonDeserialize(as = ModifiableHeadersConfiguration.class) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java index b738cbe5..c218f477 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java @@ -26,14 +26,12 @@ @AutoBind public class AuditLogResponseFilter implements ContainerResponseFilter { private final AuditLog auditLog; - private final AppContext appContext; private final List included; private final List excluded; @Inject public AuditLogResponseFilter(AuditLog auditLog, AppContext appContext) { this.auditLog = auditLog; - this.appContext = appContext; included = appContext.getConfiguration().getAuditLog().getHttpStatus().getIncluded(); excluded = appContext.getConfiguration().getAuditLog().getHttpStatus().getExcluded(); } @@ -91,8 +89,10 @@ public void filter( } try { + // Wait for the pipeline to finish (sync mode). waitForEntity(responseContext); } catch (IOException e) { + // Abort log and request on error. auditLog.abortLog(requestId); responseContext.setStatus(500); responseContext.setEntity(null); From 5135ea6a740cd12a7a61f46c5b340bdf83625df5 Mon Sep 17 00:00:00 2001 From: Afeef Neiroukh Date: Thu, 11 Jun 2026 11:50:34 +0200 Subject: [PATCH 42/44] api config: includePropertyValues --- .../services/app/AuditLogImpl.java | 26 +++++++++++++++++++ .../services/domain/AuditLog.java | 8 ++++++ 2 files changed, 34 insertions(+) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index e593f446..4c232e75 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -7,6 +7,7 @@ */ package de.ii.xtraplatform.services.app; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.core.JsonProcessingException; @@ -122,6 +123,19 @@ public void abortLog(String requestId) { auditLogMapping.remove(requestId); } + @Override + public void setIncludePropertyValues(String requestId, boolean value) { + if (isDisabled()) { + return; + } + getOptionalLog(requestId).ifPresent(log -> log.setIncludePropertyValues(value)); + } + + @Override + public boolean getIncludePropertyValues(String requestId) { + return getOptionalLog(requestId).map(Log::getIncludePropertyValues).orElse(false); + } + @Override public boolean logIsAvailable(String requestId) { return !isDisabled() && auditLogMapping.containsKey(requestId); @@ -264,6 +278,7 @@ public static class LogImpl implements Log { private Instant finished; private Map target; private String api; + private boolean includePropertyValues = true; LogImpl(String id) { this.id = id; @@ -311,6 +326,17 @@ public void setOperationStatus(String status) { operation.put("status", status); } + @JsonIgnore + @Override + public boolean getIncludePropertyValues() { + return includePropertyValues; + } + + @Override + public void setIncludePropertyValues(boolean value) { + includePropertyValues = value; + } + @JsonProperty("id") @Override public String getId() { diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java index 7444a8fc..b5590717 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/domain/AuditLog.java @@ -16,6 +16,10 @@ public interface AuditLog { void abortLog(String requestId); + void setIncludePropertyValues(String requestId, boolean value); + + boolean getIncludePropertyValues(String requestId); + boolean isEnabled(); boolean logIsAvailable(String requestId); @@ -50,6 +54,10 @@ interface Log { void setOperationStatus(String status); + void setIncludePropertyValues(boolean value); + + boolean getIncludePropertyValues(); + String getId(); Instant getStarted(); From 9c5077af83250f8730bb9a9e3116ba1748104566 Mon Sep 17 00:00:00 2001 From: Andreas Zahnen Date: Thu, 11 Jun 2026 14:02:58 +0200 Subject: [PATCH 43/44] improve error handling --- .../services/app/AuditLogImpl.java | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java index 4c232e75..f671537c 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogImpl.java @@ -17,6 +17,7 @@ import de.ii.xtraplatform.base.domain.AppContext; import de.ii.xtraplatform.base.domain.AuditLogConfiguration; import de.ii.xtraplatform.base.domain.Jackson; +import de.ii.xtraplatform.base.domain.LogContext; import de.ii.xtraplatform.blobs.domain.ResourceStore; import de.ii.xtraplatform.services.domain.AuditLog; import jakarta.inject.Inject; @@ -25,6 +26,7 @@ import jakarta.ws.rs.core.MultivaluedMap; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Instant; import java.time.ZoneOffset; @@ -41,7 +43,9 @@ @Singleton @AutoBind public class AuditLogImpl implements AuditLog { + private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogImpl.class); + private final Map auditLogMapping = new ConcurrentHashMap<>(); private final ResourceStore auditLogStore; private final AppContext appContext; @@ -69,7 +73,6 @@ private Optional getOptionalLog(String requestId) { if (auditLogMapping.containsKey(requestId)) { return Optional.of(auditLogMapping.get(requestId)); } else { - LOGGER.error("No AuditLog-object found for requestId {}", requestId); return Optional.empty(); } } @@ -219,7 +222,11 @@ public void setTarget(String requestId, Map target) { } @Override - @SuppressWarnings({"PMD.CognitiveComplexity"}) + @SuppressWarnings({ + "PMD.CognitiveComplexity", + "PMD.CyclomaticComplexity", + "PMD.AvoidInstantiatingObjectsInLoops" + }) public boolean removeAndWriteLog(String requestId) { if (isDisabled()) { return false; @@ -227,20 +234,19 @@ public boolean removeAndWriteLog(String requestId) { Log log = auditLogMapping.remove(requestId); if (Objects.isNull(log)) { - LOGGER.error("No AuditLog-object found for requestId {}", requestId); return false; } log.finish(); - final ByteArrayInputStream inputStream; + final byte[] serializedLog; try { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug(objectWriter.writeValueAsString(log)); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace(objectWriter.writeValueAsString(log)); } - inputStream = new ByteArrayInputStream(objectWriter.writeValueAsBytes(log)); + serializedLog = objectWriter.writeValueAsBytes(log); } catch (JsonProcessingException e) { - LOGGER.error("Failed to serialize log {}", requestId, e); + LOGGER.error("Failed to serialize audit log {}", requestId, e); return false; } @@ -249,17 +255,24 @@ public boolean removeAndWriteLog(String requestId) { int retries = 0; do { try { - inputStream.reset(); - auditLogStore.put(path, inputStream); + auditLogStore.put(path, new ByteArrayInputStream(serializedLog)); return true; } catch (IOException e) { - // ToDo: Delay until next try if (retries < maxRetries) { + int delay = 100 * (retries + 1); if (LOGGER.isWarnEnabled()) { - LOGGER.warn("Failed to write audit log {}, retrying...", requestId); + LOGGER.warn("Failed to write audit log {}, retrying in {}ms", requestId, delay); + } + try { + Thread.sleep(delay); + } catch (InterruptedException ex) { + // ignore } } else { - LOGGER.error("Giving up writing audit log {} after {} retries", requestId, retries, e); + LogContext.error( + LOGGER, e, "Giving up writing audit log {} after {} retries", requestId, retries + 1); + LOGGER.error( + "Audit log for {}: {}", requestId, new String(serializedLog, StandardCharsets.UTF_8)); return false; } } From 3108e8e88478bcd5ec8517cbee115630d1d4011f Mon Sep 17 00:00:00 2001 From: Andreas Zahnen Date: Thu, 11 Jun 2026 14:03:27 +0200 Subject: [PATCH 44/44] introduce JoinableStreamingOutput --- .../openapi/app/OpenApiSwaggerUiResource.java | 5 +- .../services/app/AuditLogResponseFilter.java | 74 ++++++++++--------- .../web/domain/JoinableStreamingOutput.java | 45 +++++++++++ 3 files changed, 86 insertions(+), 38 deletions(-) create mode 100644 xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/JoinableStreamingOutput.java diff --git a/xtraplatform-openapi/src/main/java/de/ii/xtraplatform/openapi/app/OpenApiSwaggerUiResource.java b/xtraplatform-openapi/src/main/java/de/ii/xtraplatform/openapi/app/OpenApiSwaggerUiResource.java index 3ae31069..ffab777d 100644 --- a/xtraplatform-openapi/src/main/java/de/ii/xtraplatform/openapi/app/OpenApiSwaggerUiResource.java +++ b/xtraplatform-openapi/src/main/java/de/ii/xtraplatform/openapi/app/OpenApiSwaggerUiResource.java @@ -14,11 +14,11 @@ import com.google.common.io.Files; import com.google.common.io.Resources; import de.ii.xtraplatform.openapi.domain.OpenApiViewerResource; +import de.ii.xtraplatform.web.domain.JoinableStreamingOutput; import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.StreamingOutput; import java.net.URL; import java.util.Objects; import org.slf4j.Logger; @@ -43,7 +43,8 @@ public Response getFile(String file) { throw new NotFoundException(); } - return Response.ok((StreamingOutput) output -> Resources.asByteSource(url).copyTo(output)) + return Response.ok( + new JoinableStreamingOutput(output -> Resources.asByteSource(url).copyTo(output))) .type(getMimeType(file)) .build(); } catch (Throwable e) { diff --git a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java index c218f477..905dba38 100644 --- a/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java +++ b/xtraplatform-services/src/main/java/de/ii/xtraplatform/services/app/AuditLogResponseFilter.java @@ -10,14 +10,14 @@ import com.github.azahnen.dagger.annotations.AutoBind; import de.ii.xtraplatform.base.domain.AppContext; import de.ii.xtraplatform.services.domain.AuditLog; +import de.ii.xtraplatform.web.domain.JoinableStreamingOutput; import jakarta.inject.Inject; import jakarta.inject.Singleton; -import jakarta.ws.rs.ServerErrorException; +import jakarta.ws.rs.InternalServerErrorException; +import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; -import jakarta.ws.rs.core.StreamingOutput; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import java.util.Objects; @@ -25,6 +25,7 @@ @Singleton @AutoBind public class AuditLogResponseFilter implements ContainerResponseFilter { + private final AuditLog auditLog; private final List included; private final List excluded; @@ -32,8 +33,8 @@ public class AuditLogResponseFilter implements ContainerResponseFilter { @Inject public AuditLogResponseFilter(AuditLog auditLog, AppContext appContext) { this.auditLog = auditLog; - included = appContext.getConfiguration().getAuditLog().getHttpStatus().getIncluded(); - excluded = appContext.getConfiguration().getAuditLog().getHttpStatus().getExcluded(); + this.included = appContext.getConfiguration().getAuditLog().getHttpStatus().getIncluded(); + this.excluded = appContext.getConfiguration().getAuditLog().getHttpStatus().getExcluded(); } private boolean sufficientHttpCode(int statusCodeInt) { @@ -44,20 +45,6 @@ private boolean sufficientHttpCode(int statusCodeInt) { && (included.contains("*") || included.contains(statusCode)); } - private void waitForEntity(ContainerResponseContext responseContext) throws IOException { - if (responseContext.getEntity() instanceof StreamingOutput streamingOutput) { - // Block until stream is finished - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - streamingOutput.write(baos); - - // Manually set entity - byte[] buffered = baos.toByteArray(); - responseContext.setEntity(buffered); - responseContext.getHeaders().putSingle("Content-Length", buffered.length); - } - } - - @SuppressWarnings("PMD.CyclomaticComplexity") @Override public void filter( ContainerRequestContext requestContext, ContainerResponseContext responseContext) @@ -88,36 +75,51 @@ public void filter( return; } - try { - // Wait for the pipeline to finish (sync mode). - waitForEntity(responseContext); - } catch (IOException e) { - // Abort log and request on error. - auditLog.abortLog(requestId); - responseContext.setStatus(500); - responseContext.setEntity(null); - responseContext.getHeaders().clear(); - throw new ServerErrorException("Internal Server Error", 500, e); + if (responseContext.getEntity() instanceof JoinableStreamingOutput streamingOutput) { + streamingOutput.whenComplete( + (throwable) -> { + int statusCode = getStatusCode(responseContext.getStatus(), throwable); + + writeLogEntry(requestId, statusCode); + }); + return; } + writeLogEntry(requestId, responseContext.getStatus()); + } + + private void writeLogEntry(String requestId, int statusCode) { // Abort log and return if HTTP-Code is not applicable according to the global config - if (!sufficientHttpCode(responseContext.getStatus())) { + if (!sufficientHttpCode(statusCode)) { auditLog.abortLog(requestId); return; } - // Log status - auditLog.setOperationStatus(requestId, Integer.toString(responseContext.getStatus())); + auditLog.setOperationStatus(requestId, Integer.toString(statusCode)); // Write the final log and save result boolean logSuccessful = auditLog.removeAndWriteLog(requestId); // Abort request if writing the log was not successful! if (!logSuccessful) { - responseContext.setStatus(500); - responseContext.setEntity(null); - responseContext.getHeaders().clear(); - throw new ServerErrorException("Internal Server Error", 500); + throw new InternalServerErrorException(); + } + } + + // This is not one hundred percent correct. If the web server already started writing the response + // to the client before an exception occurred, the returned status code will always be 200, even + // if the response is broken and the log shows an error. But there is no way to determine this + // case, so we log the status code that should have been returned. + private static int getStatusCode(int initialStatusCode, Throwable throwable) { + int statusCode = initialStatusCode; + + if (Objects.nonNull(throwable)) { + if (throwable instanceof WebApplicationException webAppException) { + statusCode = webAppException.getResponse().getStatus(); + } else { + statusCode = 500; + } } + return statusCode; } } diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/JoinableStreamingOutput.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/JoinableStreamingOutput.java new file mode 100644 index 00000000..7217d4f2 --- /dev/null +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/JoinableStreamingOutput.java @@ -0,0 +1,45 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.web.domain; + +import jakarta.ws.rs.core.StreamingOutput; +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +public class JoinableStreamingOutput implements StreamingOutput { + + private final StreamingOutput delegate; + private final CompletableFuture completableFuture; + + public JoinableStreamingOutput(StreamingOutput delegate) { + this.delegate = delegate; + this.completableFuture = new CompletableFuture<>(); + } + + public void whenComplete(Consumer onComplete) { + completableFuture.whenComplete( + (result, throwable) -> { + onComplete.accept(throwable); + }); + } + + @Override + @SuppressWarnings({"PMD.AvoidCatchingGenericException"}) + public void write(OutputStream outputStream) throws IOException { + try { + delegate.write(outputStream); + } catch (Throwable throwable) { + completableFuture.completeExceptionally(throwable); + throw throwable; + } finally { + completableFuture.complete(null); + } + } +}