From ac3662e7584cba1edb67c9754e3470984e680f0f Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Wed, 8 Apr 2026 06:26:54 +0200 Subject: [PATCH] =?UTF-8?q?M7=20N2=20Logging-Sensitivit=C3=A4t=20produktiv?= =?UTF-8?q?=20verdrahtet=20und=20verifiziert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/out/ProcessingLogger.java | 39 ++++++++++++++ .../DocumentProcessingCoordinator.java | 10 ++++ .../DocumentProcessingCoordinatorTest.java | 11 ++++ .../BatchRunProcessingUseCaseTest.java | 18 +++++++ .../umbenenner/bootstrap/BootstrapRunner.java | 9 ++-- .../adapter/Log4jProcessingLogger.java | 41 ++++++++++++++- .../adapter/Log4jProcessingLoggerTest.java | 51 +++++++++++++++++++ 7 files changed, 175 insertions(+), 4 deletions(-) diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/ProcessingLogger.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/ProcessingLogger.java index 61f218c..0210f7b 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/ProcessingLogger.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/ProcessingLogger.java @@ -5,6 +5,22 @@ package de.gecheckt.pdf.umbenenner.application.port.out; *

* The application delegates all logging to this port to remain decoupled from * specific logging frameworks. Concrete implementations are provided by adapters. + *

+ *

Sensitive AI content logging

+ *

+ * The {@link #debugSensitiveAiContent(String, Object[])} method allows logging + * of sensitive AI-generated content (complete raw response, complete reasoning) + * subject to the {@link AiContentSensitivity} setting: + *

+ *

+ * The complete sensitive content is always persisted in SQLite for traceability, + * regardless of this logging setting. The logging decision controls only whether + * the content also appears in log files. */ public interface ProcessingLogger { @@ -24,6 +40,29 @@ public interface ProcessingLogger { */ void debug(String message, Object... args); + /** + * Logs a debug-level message containing sensitive AI-generated content, + * subject to the configured {@link AiContentSensitivity}. + *

+ * This method is called with message and arguments containing sensitive AI content + * (e.g., complete raw response, complete reasoning). The implementation must: + *

+ *

+ * This is the only method where sensitive AI content may be logged based on + * configuration. Other logging methods ({@link #info}, {@link #debug}, etc.) + * must never log sensitive content. + * + * @param message the message template (may contain {} placeholders) + * @param args optional message arguments that may include sensitive AI content + */ + void debugSensitiveAiContent(String message, Object... args); + /** * Logs a warning-level message. * diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinator.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinator.java index 9ccc80e..e5e722c 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinator.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinator.java @@ -393,6 +393,16 @@ public class DocumentProcessingCoordinator { "Status is PROPOSAL_READY but no PROPOSAL_READY attempt exists in history"); } + // Log sensitive AI content (raw response, reasoning) if configured + if (proposalAttempt.aiRawResponse() != null) { + logger.debugSensitiveAiContent("AI raw response for '{}' (fingerprint: {}): {}", + candidate.uniqueIdentifier(), fingerprint.sha256Hex(), proposalAttempt.aiRawResponse()); + } + if (proposalAttempt.aiReasoning() != null) { + logger.debugSensitiveAiContent("AI reasoning for '{}' (fingerprint: {}): {}", + candidate.uniqueIdentifier(), fingerprint.sha256Hex(), proposalAttempt.aiReasoning()); + } + // --- Step 2: Build base filename from the proposal --- TargetFilenameBuildingService.BaseFilenameResult filenameResult = TargetFilenameBuildingService.buildBaseFilename(proposalAttempt); diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinatorTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinatorTest.java index 810b471..a00b210 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinatorTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinatorTest.java @@ -1280,6 +1280,11 @@ class DocumentProcessingCoordinatorTest { // No-op } + @Override + public void debugSensitiveAiContent(String message, Object... args) { + // No-op: sensitivity is controlled by the Log4jProcessingLogger adapter + } + @Override public void warn(String message, Object... args) { // No-op @@ -1367,6 +1372,7 @@ class DocumentProcessingCoordinatorTest { private static class CapturingProcessingLogger implements ProcessingLogger { int infoCallCount = 0; int debugCallCount = 0; + int debugSensitiveAiContentCallCount = 0; int warnCallCount = 0; int errorCallCount = 0; @@ -1380,6 +1386,11 @@ class DocumentProcessingCoordinatorTest { debugCallCount++; } + @Override + public void debugSensitiveAiContent(String message, Object... args) { + debugSensitiveAiContentCallCount++; + } + @Override public void warn(String message, Object... args) { warnCallCount++; diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java index 2eeb5f3..1e65278 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java @@ -1157,6 +1157,11 @@ class BatchRunProcessingUseCaseTest { // No-op } + @Override + public void debugSensitiveAiContent(String message, Object... args) { + // No-op: sensitivity is controlled by the Log4jProcessingLogger adapter + } + @Override public void warn(String message, Object... args) { // No-op @@ -1175,6 +1180,7 @@ class BatchRunProcessingUseCaseTest { private static class MessageCapturingProcessingLogger implements ProcessingLogger { final List infoMessages = new ArrayList<>(); final List debugMessages = new ArrayList<>(); + final List debugSensitiveAiContentMessages = new ArrayList<>(); final List warnMessages = new ArrayList<>(); final List errorMessages = new ArrayList<>(); @@ -1204,6 +1210,11 @@ class BatchRunProcessingUseCaseTest { debugMessages.add(format(message, args)); } + @Override + public void debugSensitiveAiContent(String message, Object... args) { + debugSensitiveAiContentMessages.add(format(message, args)); + } + @Override public void warn(String message, Object... args) { warnMessages.add(format(message, args)); @@ -1218,6 +1229,7 @@ class BatchRunProcessingUseCaseTest { List all = new ArrayList<>(); all.addAll(infoMessages); all.addAll(debugMessages); + all.addAll(debugSensitiveAiContentMessages); all.addAll(warnMessages); all.addAll(errorMessages); return all; @@ -1228,6 +1240,7 @@ class BatchRunProcessingUseCaseTest { private static class CapturingProcessingLogger implements ProcessingLogger { int infoCallCount = 0; int debugCallCount = 0; + int debugSensitiveAiContentCallCount = 0; int warnCallCount = 0; int errorCallCount = 0; @@ -1241,6 +1254,11 @@ class BatchRunProcessingUseCaseTest { debugCallCount++; } + @Override + public void debugSensitiveAiContent(String message, Object... args) { + debugSensitiveAiContentCallCount++; + } + @Override public void warn(String message, Object... args) { warnCallCount++; diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java index 4d0f3eb..b1e6a94 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java @@ -205,7 +205,8 @@ public class BootstrapRunner { this.schemaInitPortFactory = SqliteSchemaInitializationAdapter::new; this.useCaseFactory = (startConfig, lock) -> { // Extract runtime configuration from startup configuration - RuntimeConfiguration runtimeConfig = new RuntimeConfiguration(startConfig.maxPages(), startConfig.maxRetriesTransient(), resolveAiContentSensitivity(startConfig.logAiSensitive())); + AiContentSensitivity aiContentSensitivity = resolveAiContentSensitivity(startConfig.logAiSensitive()); + RuntimeConfiguration runtimeConfig = new RuntimeConfiguration(startConfig.maxPages(), startConfig.maxRetriesTransient(), aiContentSensitivity); String jdbcUrl = buildJdbcUrl(startConfig); FingerprintPort fingerprintPort = new Sha256FingerprintAdapter(); @@ -215,7 +216,8 @@ public class BootstrapRunner { new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl); UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl); - ProcessingLogger coordinatorLogger = new Log4jProcessingLogger(DocumentProcessingCoordinator.class); + // Wire coordinators logger with AI content sensitivity setting + ProcessingLogger coordinatorLogger = new Log4jProcessingLogger(DocumentProcessingCoordinator.class, aiContentSensitivity); TargetFolderPort targetFolderPort = new FilesystemTargetFolderAdapter(startConfig.targetFolder()); TargetFileCopyPort targetFileCopyPort = new FilesystemTargetFileCopyAdapter(startConfig.targetFolder()); DocumentProcessingCoordinator documentProcessingCoordinator = @@ -235,7 +237,8 @@ public class BootstrapRunner { startConfig.apiModel(), startConfig.maxTextCharacters()); - ProcessingLogger useCaseLogger = new Log4jProcessingLogger(DefaultBatchRunProcessingUseCase.class); + // Wire use case logger with AI content sensitivity setting + ProcessingLogger useCaseLogger = new Log4jProcessingLogger(DefaultBatchRunProcessingUseCase.class, aiContentSensitivity); return new DefaultBatchRunProcessingUseCase( runtimeConfig, lock, diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jProcessingLogger.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jProcessingLogger.java index 9d103c5..331bf5b 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jProcessingLogger.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jProcessingLogger.java @@ -1,10 +1,13 @@ package de.gecheckt.pdf.umbenenner.bootstrap.adapter; +import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.Objects; + /** * Log4j-based adapter implementing the {@link ProcessingLogger} port. *

@@ -13,18 +16,45 @@ import org.apache.logging.log4j.Logger; *

* The error method intelligently detects if the last argument is a Throwable * and logs accordingly. + *

+ *

Sensitive AI content control

+ *

+ * The adapter is initialized with an {@link AiContentSensitivity} setting that + * controls whether sensitive AI-generated content (complete raw response, reasoning) + * may be written to log files: + *

*/ public class Log4jProcessingLogger implements ProcessingLogger { private final Logger log4jLogger; + private final AiContentSensitivity aiContentSensitivity; /** - * Creates a logger instance for the given class. + * Creates a logger instance for the given class with default sensitivity setting. + *

+ * Uses {@link AiContentSensitivity#PROTECT_SENSITIVE_CONTENT} as the default. * * @param clazz the class to derive the logger name from; must not be null */ public Log4jProcessingLogger(Class clazz) { + this(clazz, AiContentSensitivity.PROTECT_SENSITIVE_CONTENT); + } + + /** + * Creates a logger instance for the given class with the specified sensitivity setting. + * + * @param clazz the class to derive the logger name from; must not be null + * @param aiContentSensitivity the sensitivity setting for AI content logging; must not be null + */ + public Log4jProcessingLogger(Class clazz, AiContentSensitivity aiContentSensitivity) { this.log4jLogger = LogManager.getLogger(clazz); + this.aiContentSensitivity = Objects.requireNonNull(aiContentSensitivity, + "aiContentSensitivity must not be null"); } @Override @@ -37,6 +67,15 @@ public class Log4jProcessingLogger implements ProcessingLogger { log4jLogger.debug(message, args); } + @Override + public void debugSensitiveAiContent(String message, Object... args) { + // Only log sensitive content if explicitly enabled + if (aiContentSensitivity == AiContentSensitivity.LOG_SENSITIVE_CONTENT) { + log4jLogger.debug(message, args); + } + // Otherwise emit nothing (protect by default) + } + @Override public void warn(String message, Object... args) { log4jLogger.warn(message, args); diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jProcessingLoggerTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jProcessingLoggerTest.java index 8625318..3083aba 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jProcessingLoggerTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jProcessingLoggerTest.java @@ -1,5 +1,6 @@ package de.gecheckt.pdf.umbenenner.bootstrap.adapter; +import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; @@ -188,4 +189,54 @@ class Log4jProcessingLoggerTest { () -> assertDoesNotThrow(() -> logger.error(testMessage, exception)) ); } + + @Test + void debugSensitiveAiContent_withDefaultSensitivity_acceptsMessage() { + // Verify debugSensitiveAiContent method exists and executes with default PROTECT sensitivity + Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger( + Log4jProcessingLoggerTest.class, + AiContentSensitivity.PROTECT_SENSITIVE_CONTENT); + + assertDoesNotThrow(() -> { + protectedLogger.debugSensitiveAiContent("Sensitive content: {}", "raw AI response"); + }, "debugSensitiveAiContent() should execute without throwing with PROTECT_SENSITIVE_CONTENT"); + } + + @Test + void debugSensitiveAiContent_withLogSensitivity_acceptsMessage() { + // Verify debugSensitiveAiContent method executes with LOG_SENSITIVE_CONTENT setting + Log4jProcessingLogger logSensitiveLogger = new Log4jProcessingLogger( + Log4jProcessingLoggerTest.class, + AiContentSensitivity.LOG_SENSITIVE_CONTENT); + + assertDoesNotThrow(() -> { + logSensitiveLogger.debugSensitiveAiContent("AI reasoning: {}", "complete reasoning text"); + }, "debugSensitiveAiContent() should execute without throwing with LOG_SENSITIVE_CONTENT"); + } + + @Test + void constructorWithSensitivity_acceptsProtectSensitiveContent() { + // Verify constructor accepts PROTECT_SENSITIVE_CONTENT + Log4jProcessingLogger logger1 = new Log4jProcessingLogger( + Log4jProcessingLoggerTest.class, + AiContentSensitivity.PROTECT_SENSITIVE_CONTENT); + assertNotNull(logger1, "Logger should be created with PROTECT_SENSITIVE_CONTENT"); + } + + @Test + void constructorWithSensitivity_acceptsLogSensitiveContent() { + // Verify constructor accepts LOG_SENSITIVE_CONTENT + Log4jProcessingLogger logger2 = new Log4jProcessingLogger( + Log4jProcessingLoggerTest.class, + AiContentSensitivity.LOG_SENSITIVE_CONTENT); + assertNotNull(logger2, "Logger should be created with LOG_SENSITIVE_CONTENT"); + } + + @Test + void constructorWithSensitivity_rejectNullSensitivity() { + // Verify constructor requires non-null AiContentSensitivity + assertThrows(NullPointerException.class, () -> { + new Log4jProcessingLogger(Log4jProcessingLoggerTest.class, null); + }, "Constructor should reject null AiContentSensitivity"); + } }