M7 N2 Logging-Sensitivität produktiv verdrahtet und verifiziert
This commit is contained in:
@@ -5,6 +5,22 @@ package de.gecheckt.pdf.umbenenner.application.port.out;
|
|||||||
* <p>
|
* <p>
|
||||||
* The application delegates all logging to this port to remain decoupled from
|
* The application delegates all logging to this port to remain decoupled from
|
||||||
* specific logging frameworks. Concrete implementations are provided by adapters.
|
* specific logging frameworks. Concrete implementations are provided by adapters.
|
||||||
|
* <p>
|
||||||
|
* <h2>Sensitive AI content logging</h2>
|
||||||
|
* <p>
|
||||||
|
* The {@link #debugSensitiveAiContent(String, Object[])} method allows logging
|
||||||
|
* of sensitive AI-generated content (complete raw response, complete reasoning)
|
||||||
|
* subject to the {@link AiContentSensitivity} setting:
|
||||||
|
* <ul>
|
||||||
|
* <li>When {@link AiContentSensitivity#PROTECT_SENSITIVE_CONTENT} is active (default),
|
||||||
|
* this method logs nothing.</li>
|
||||||
|
* <li>When {@link AiContentSensitivity#LOG_SENSITIVE_CONTENT} is explicitly enabled,
|
||||||
|
* this method logs the content to DEBUG level.</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* 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 {
|
public interface ProcessingLogger {
|
||||||
|
|
||||||
@@ -24,6 +40,29 @@ public interface ProcessingLogger {
|
|||||||
*/
|
*/
|
||||||
void debug(String message, Object... args);
|
void debug(String message, Object... args);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a debug-level message containing sensitive AI-generated content,
|
||||||
|
* subject to the configured {@link AiContentSensitivity}.
|
||||||
|
* <p>
|
||||||
|
* This method is called with message and arguments containing sensitive AI content
|
||||||
|
* (e.g., complete raw response, complete reasoning). The implementation must:
|
||||||
|
* <ul>
|
||||||
|
* <li>Check the current {@link AiContentSensitivity} setting.</li>
|
||||||
|
* <li>If set to {@link AiContentSensitivity#PROTECT_SENSITIVE_CONTENT} (default),
|
||||||
|
* emit nothing to log.</li>
|
||||||
|
* <li>If set to {@link AiContentSensitivity#LOG_SENSITIVE_CONTENT}, log the
|
||||||
|
* message and arguments at DEBUG level normally.</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* 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.
|
* Logs a warning-level message.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -393,6 +393,16 @@ public class DocumentProcessingCoordinator {
|
|||||||
"Status is PROPOSAL_READY but no PROPOSAL_READY attempt exists in history");
|
"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 ---
|
// --- Step 2: Build base filename from the proposal ---
|
||||||
TargetFilenameBuildingService.BaseFilenameResult filenameResult =
|
TargetFilenameBuildingService.BaseFilenameResult filenameResult =
|
||||||
TargetFilenameBuildingService.buildBaseFilename(proposalAttempt);
|
TargetFilenameBuildingService.buildBaseFilename(proposalAttempt);
|
||||||
|
|||||||
@@ -1280,6 +1280,11 @@ class DocumentProcessingCoordinatorTest {
|
|||||||
// No-op
|
// No-op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void debugSensitiveAiContent(String message, Object... args) {
|
||||||
|
// No-op: sensitivity is controlled by the Log4jProcessingLogger adapter
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void warn(String message, Object... args) {
|
public void warn(String message, Object... args) {
|
||||||
// No-op
|
// No-op
|
||||||
@@ -1367,6 +1372,7 @@ class DocumentProcessingCoordinatorTest {
|
|||||||
private static class CapturingProcessingLogger implements ProcessingLogger {
|
private static class CapturingProcessingLogger implements ProcessingLogger {
|
||||||
int infoCallCount = 0;
|
int infoCallCount = 0;
|
||||||
int debugCallCount = 0;
|
int debugCallCount = 0;
|
||||||
|
int debugSensitiveAiContentCallCount = 0;
|
||||||
int warnCallCount = 0;
|
int warnCallCount = 0;
|
||||||
int errorCallCount = 0;
|
int errorCallCount = 0;
|
||||||
|
|
||||||
@@ -1380,6 +1386,11 @@ class DocumentProcessingCoordinatorTest {
|
|||||||
debugCallCount++;
|
debugCallCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void debugSensitiveAiContent(String message, Object... args) {
|
||||||
|
debugSensitiveAiContentCallCount++;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void warn(String message, Object... args) {
|
public void warn(String message, Object... args) {
|
||||||
warnCallCount++;
|
warnCallCount++;
|
||||||
|
|||||||
@@ -1157,6 +1157,11 @@ class BatchRunProcessingUseCaseTest {
|
|||||||
// No-op
|
// No-op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void debugSensitiveAiContent(String message, Object... args) {
|
||||||
|
// No-op: sensitivity is controlled by the Log4jProcessingLogger adapter
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void warn(String message, Object... args) {
|
public void warn(String message, Object... args) {
|
||||||
// No-op
|
// No-op
|
||||||
@@ -1175,6 +1180,7 @@ class BatchRunProcessingUseCaseTest {
|
|||||||
private static class MessageCapturingProcessingLogger implements ProcessingLogger {
|
private static class MessageCapturingProcessingLogger implements ProcessingLogger {
|
||||||
final List<String> infoMessages = new ArrayList<>();
|
final List<String> infoMessages = new ArrayList<>();
|
||||||
final List<String> debugMessages = new ArrayList<>();
|
final List<String> debugMessages = new ArrayList<>();
|
||||||
|
final List<String> debugSensitiveAiContentMessages = new ArrayList<>();
|
||||||
final List<String> warnMessages = new ArrayList<>();
|
final List<String> warnMessages = new ArrayList<>();
|
||||||
final List<String> errorMessages = new ArrayList<>();
|
final List<String> errorMessages = new ArrayList<>();
|
||||||
|
|
||||||
@@ -1204,6 +1210,11 @@ class BatchRunProcessingUseCaseTest {
|
|||||||
debugMessages.add(format(message, args));
|
debugMessages.add(format(message, args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void debugSensitiveAiContent(String message, Object... args) {
|
||||||
|
debugSensitiveAiContentMessages.add(format(message, args));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void warn(String message, Object... args) {
|
public void warn(String message, Object... args) {
|
||||||
warnMessages.add(format(message, args));
|
warnMessages.add(format(message, args));
|
||||||
@@ -1218,6 +1229,7 @@ class BatchRunProcessingUseCaseTest {
|
|||||||
List<String> all = new ArrayList<>();
|
List<String> all = new ArrayList<>();
|
||||||
all.addAll(infoMessages);
|
all.addAll(infoMessages);
|
||||||
all.addAll(debugMessages);
|
all.addAll(debugMessages);
|
||||||
|
all.addAll(debugSensitiveAiContentMessages);
|
||||||
all.addAll(warnMessages);
|
all.addAll(warnMessages);
|
||||||
all.addAll(errorMessages);
|
all.addAll(errorMessages);
|
||||||
return all;
|
return all;
|
||||||
@@ -1228,6 +1240,7 @@ class BatchRunProcessingUseCaseTest {
|
|||||||
private static class CapturingProcessingLogger implements ProcessingLogger {
|
private static class CapturingProcessingLogger implements ProcessingLogger {
|
||||||
int infoCallCount = 0;
|
int infoCallCount = 0;
|
||||||
int debugCallCount = 0;
|
int debugCallCount = 0;
|
||||||
|
int debugSensitiveAiContentCallCount = 0;
|
||||||
int warnCallCount = 0;
|
int warnCallCount = 0;
|
||||||
int errorCallCount = 0;
|
int errorCallCount = 0;
|
||||||
|
|
||||||
@@ -1241,6 +1254,11 @@ class BatchRunProcessingUseCaseTest {
|
|||||||
debugCallCount++;
|
debugCallCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void debugSensitiveAiContent(String message, Object... args) {
|
||||||
|
debugSensitiveAiContentCallCount++;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void warn(String message, Object... args) {
|
public void warn(String message, Object... args) {
|
||||||
warnCallCount++;
|
warnCallCount++;
|
||||||
|
|||||||
@@ -205,7 +205,8 @@ public class BootstrapRunner {
|
|||||||
this.schemaInitPortFactory = SqliteSchemaInitializationAdapter::new;
|
this.schemaInitPortFactory = SqliteSchemaInitializationAdapter::new;
|
||||||
this.useCaseFactory = (startConfig, lock) -> {
|
this.useCaseFactory = (startConfig, lock) -> {
|
||||||
// Extract runtime configuration from startup configuration
|
// 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);
|
String jdbcUrl = buildJdbcUrl(startConfig);
|
||||||
FingerprintPort fingerprintPort = new Sha256FingerprintAdapter();
|
FingerprintPort fingerprintPort = new Sha256FingerprintAdapter();
|
||||||
@@ -215,7 +216,8 @@ public class BootstrapRunner {
|
|||||||
new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl);
|
new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl);
|
||||||
UnitOfWorkPort unitOfWorkPort =
|
UnitOfWorkPort unitOfWorkPort =
|
||||||
new SqliteUnitOfWorkAdapter(jdbcUrl);
|
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());
|
TargetFolderPort targetFolderPort = new FilesystemTargetFolderAdapter(startConfig.targetFolder());
|
||||||
TargetFileCopyPort targetFileCopyPort = new FilesystemTargetFileCopyAdapter(startConfig.targetFolder());
|
TargetFileCopyPort targetFileCopyPort = new FilesystemTargetFileCopyAdapter(startConfig.targetFolder());
|
||||||
DocumentProcessingCoordinator documentProcessingCoordinator =
|
DocumentProcessingCoordinator documentProcessingCoordinator =
|
||||||
@@ -235,7 +237,8 @@ public class BootstrapRunner {
|
|||||||
startConfig.apiModel(),
|
startConfig.apiModel(),
|
||||||
startConfig.maxTextCharacters());
|
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(
|
return new DefaultBatchRunProcessingUseCase(
|
||||||
runtimeConfig,
|
runtimeConfig,
|
||||||
lock,
|
lock,
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package de.gecheckt.pdf.umbenenner.bootstrap.adapter;
|
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 de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log4j-based adapter implementing the {@link ProcessingLogger} port.
|
* Log4j-based adapter implementing the {@link ProcessingLogger} port.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -13,18 +16,45 @@ import org.apache.logging.log4j.Logger;
|
|||||||
* <p>
|
* <p>
|
||||||
* The error method intelligently detects if the last argument is a Throwable
|
* The error method intelligently detects if the last argument is a Throwable
|
||||||
* and logs accordingly.
|
* and logs accordingly.
|
||||||
|
* <p>
|
||||||
|
* <h2>Sensitive AI content control</h2>
|
||||||
|
* <p>
|
||||||
|
* 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:
|
||||||
|
* <ul>
|
||||||
|
* <li>When set to {@link AiContentSensitivity#PROTECT_SENSITIVE_CONTENT} (default),
|
||||||
|
* calls to {@link #debugSensitiveAiContent(String, Object[])} emit nothing.</li>
|
||||||
|
* <li>When set to {@link AiContentSensitivity#LOG_SENSITIVE_CONTENT}, sensitive
|
||||||
|
* content is logged at DEBUG level.</li>
|
||||||
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public class Log4jProcessingLogger implements ProcessingLogger {
|
public class Log4jProcessingLogger implements ProcessingLogger {
|
||||||
|
|
||||||
private final Logger log4jLogger;
|
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.
|
||||||
|
* <p>
|
||||||
|
* Uses {@link AiContentSensitivity#PROTECT_SENSITIVE_CONTENT} as the default.
|
||||||
*
|
*
|
||||||
* @param clazz the class to derive the logger name from; must not be null
|
* @param clazz the class to derive the logger name from; must not be null
|
||||||
*/
|
*/
|
||||||
public Log4jProcessingLogger(Class<?> clazz) {
|
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.log4jLogger = LogManager.getLogger(clazz);
|
||||||
|
this.aiContentSensitivity = Objects.requireNonNull(aiContentSensitivity,
|
||||||
|
"aiContentSensitivity must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -37,6 +67,15 @@ public class Log4jProcessingLogger implements ProcessingLogger {
|
|||||||
log4jLogger.debug(message, args);
|
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
|
@Override
|
||||||
public void warn(String message, Object... args) {
|
public void warn(String message, Object... args) {
|
||||||
log4jLogger.warn(message, args);
|
log4jLogger.warn(message, args);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.gecheckt.pdf.umbenenner.bootstrap.adapter;
|
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 de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@@ -188,4 +189,54 @@ class Log4jProcessingLoggerTest {
|
|||||||
() -> assertDoesNotThrow(() -> logger.error(testMessage, exception))
|
() -> 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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user