1
0

Nachbearbeitung: Logging aus der Application-Schicht entkoppelt

This commit is contained in:
2026-04-04 14:31:14 +02:00
parent deaa8c9fa3
commit 8e6d745e4b
9 changed files with 210 additions and 55 deletions

View File

@@ -35,6 +35,10 @@
<groupId>org.json</groupId> <groupId>org.json</groupId>
<artifactId>json</artifactId> <artifactId>json</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<!-- Test dependencies --> <!-- Test dependencies -->
<dependency> <dependency>

View File

@@ -19,12 +19,6 @@
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!-- Logging -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<!-- Test dependencies --> <!-- Test dependencies -->
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>

View File

@@ -0,0 +1,46 @@
package de.gecheckt.pdf.umbenenner.application.port.out;
/**
* Outbound port for processing-related logging operations.
* <p>
* The application delegates all logging to this port to remain decoupled from
* specific logging frameworks. Concrete implementations are provided by adapters.
*/
public interface ProcessingLogger {
/**
* Logs an information-level message.
*
* @param message the message template (may contain {} placeholders)
* @param args optional message arguments for placeholder substitution
*/
void info(String message, Object... args);
/**
* Logs a debug-level message.
*
* @param message the message template (may contain {} placeholders)
* @param args optional message arguments for placeholder substitution
*/
void debug(String message, Object... args);
/**
* Logs a warning-level message.
*
* @param message the message template (may contain {} placeholders)
* @param args optional message arguments for placeholder substitution
*/
void warn(String message, Object... args);
/**
* Logs an error-level message with optional arguments and exception.
* <p>
* If the last argument is a Throwable, it is treated as the exception to log.
* Otherwise, all arguments are treated as message parameters.
*
* @param message the message template (may contain {} placeholders)
* @param args optional message arguments and/or exception
*/
void error(String message, Object... args);
}

View File

@@ -12,6 +12,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceLookupTechnicalFailure; import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceLookupTechnicalFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort; import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext; import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint; import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
@@ -22,9 +23,6 @@ import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator; import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator;
import de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError; import de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.time.Instant; import java.time.Instant;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -80,14 +78,13 @@ import java.util.function.Function;
*/ */
public class DocumentProcessingCoordinator { public class DocumentProcessingCoordinator {
private static final Logger LOG = LogManager.getLogger(DocumentProcessingCoordinator.class);
private final DocumentRecordRepository documentRecordRepository; private final DocumentRecordRepository documentRecordRepository;
private final ProcessingAttemptRepository processingAttemptRepository; private final ProcessingAttemptRepository processingAttemptRepository;
private final UnitOfWorkPort unitOfWorkPort; private final UnitOfWorkPort unitOfWorkPort;
private final ProcessingLogger logger;
/** /**
* Creates the document processor with the required persistence ports. * Creates the document processor with the required persistence ports and logger.
* *
* @param documentRecordRepository port for reading and writing the document master record; * @param documentRecordRepository port for reading and writing the document master record;
* must not be null * must not be null
@@ -95,18 +92,21 @@ public class DocumentProcessingCoordinator {
* must not be null * must not be null
* @param unitOfWorkPort port for executing operations atomically; * @param unitOfWorkPort port for executing operations atomically;
* must not be null * must not be null
* @param logger for processing-related logging; must not be null
* @throws NullPointerException if any parameter is null * @throws NullPointerException if any parameter is null
*/ */
public DocumentProcessingCoordinator( public DocumentProcessingCoordinator(
DocumentRecordRepository documentRecordRepository, DocumentRecordRepository documentRecordRepository,
ProcessingAttemptRepository processingAttemptRepository, ProcessingAttemptRepository processingAttemptRepository,
UnitOfWorkPort unitOfWorkPort) { UnitOfWorkPort unitOfWorkPort,
ProcessingLogger logger) {
this.documentRecordRepository = this.documentRecordRepository =
Objects.requireNonNull(documentRecordRepository, "documentRecordRepository must not be null"); Objects.requireNonNull(documentRecordRepository, "documentRecordRepository must not be null");
this.processingAttemptRepository = this.processingAttemptRepository =
Objects.requireNonNull(processingAttemptRepository, "processingAttemptRepository must not be null"); Objects.requireNonNull(processingAttemptRepository, "processingAttemptRepository must not be null");
this.unitOfWorkPort = this.unitOfWorkPort =
Objects.requireNonNull(unitOfWorkPort, "unitOfWorkPort must not be null"); Objects.requireNonNull(unitOfWorkPort, "unitOfWorkPort must not be null");
this.logger = Objects.requireNonNull(logger, "logger must not be null");
} }
/** /**
@@ -192,7 +192,7 @@ public class DocumentProcessingCoordinator {
// Step 2: Handle persistence lookup failure cannot safely proceed // Step 2: Handle persistence lookup failure cannot safely proceed
if (lookupResult instanceof PersistenceLookupTechnicalFailure failure) { if (lookupResult instanceof PersistenceLookupTechnicalFailure failure) {
LOG.error("Cannot process '{}': master record lookup failed: {}", logger.error("Cannot process '{}': master record lookup failed: {}",
candidate.uniqueIdentifier(), failure.errorMessage()); candidate.uniqueIdentifier(), failure.errorMessage());
return; return;
} }
@@ -201,7 +201,7 @@ public class DocumentProcessingCoordinator {
switch (lookupResult) { switch (lookupResult) {
case DocumentTerminalSuccess terminalSuccess -> { case DocumentTerminalSuccess terminalSuccess -> {
// Document already successfully processed → skip // Document already successfully processed → skip
LOG.info("Skipping '{}': already successfully processed (fingerprint: {}).", logger.info("Skipping '{}': already successfully processed (fingerprint: {}).",
candidate.uniqueIdentifier(), fingerprint.sha256Hex()); candidate.uniqueIdentifier(), fingerprint.sha256Hex());
persistSkipAttempt( persistSkipAttempt(
candidate, fingerprint, terminalSuccess.record(), candidate, fingerprint, terminalSuccess.record(),
@@ -211,7 +211,7 @@ public class DocumentProcessingCoordinator {
case DocumentTerminalFinalFailure terminalFailure -> { case DocumentTerminalFinalFailure terminalFailure -> {
// Document finally failed → skip // Document finally failed → skip
LOG.info("Skipping '{}': already finally failed (fingerprint: {}).", logger.info("Skipping '{}': already finally failed (fingerprint: {}).",
candidate.uniqueIdentifier(), fingerprint.sha256Hex()); candidate.uniqueIdentifier(), fingerprint.sha256Hex());
persistSkipAttempt( persistSkipAttempt(
candidate, fingerprint, terminalFailure.record(), candidate, fingerprint, terminalFailure.record(),
@@ -235,7 +235,7 @@ public class DocumentProcessingCoordinator {
default -> default ->
// Exhaustive sealed hierarchy; this branch is unreachable // Exhaustive sealed hierarchy; this branch is unreachable
LOG.error("Unexpected lookup result type for '{}': {}", logger.error("Unexpected lookup result type for '{}': {}",
candidate.uniqueIdentifier(), lookupResult.getClass().getSimpleName()); candidate.uniqueIdentifier(), lookupResult.getClass().getSimpleName());
} }
} }
@@ -291,11 +291,11 @@ public class DocumentProcessingCoordinator {
txOps.updateDocumentRecord(skipRecord); txOps.updateDocumentRecord(skipRecord);
}); });
LOG.debug("Skip attempt #{} persisted for '{}' with status {}.", logger.debug("Skip attempt #{} persisted for '{}' with status {}.",
attemptNumber, candidate.uniqueIdentifier(), skipStatus); attemptNumber, candidate.uniqueIdentifier(), skipStatus);
} catch (DocumentPersistenceException e) { } catch (DocumentPersistenceException e) {
LOG.error("Failed to persist skip attempt for '{}': {}", logger.error("Failed to persist skip attempt for '{}': {}",
candidate.uniqueIdentifier(), e.getMessage(), e); candidate.uniqueIdentifier(), e.getMessage(), e);
} }
} }
@@ -514,14 +514,14 @@ public class DocumentProcessingCoordinator {
recordWriter.accept(txOps); recordWriter.accept(txOps);
}); });
LOG.info("Document '{}' processed: status={}, contentErrors={}, transientErrors={}.", logger.info("Document '{}' processed: status={}, contentErrors={}, transientErrors={}.",
candidate.uniqueIdentifier(), candidate.uniqueIdentifier(),
outcome.overallStatus(), outcome.overallStatus(),
outcome.counters().contentErrorCount(), outcome.counters().contentErrorCount(),
outcome.counters().transientErrorCount()); outcome.counters().transientErrorCount());
} catch (DocumentPersistenceException e) { } catch (DocumentPersistenceException e) {
LOG.error("Failed to persist processing result for '{}': {}", logger.error("Failed to persist processing result for '{}': {}",
candidate.uniqueIdentifier(), e.getMessage(), e); candidate.uniqueIdentifier(), e.getMessage(), e);
} }
} }

View File

@@ -8,6 +8,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintResult;
import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintSuccess; import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintTechnicalError; import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintTechnicalError;
import de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort; import de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException;
import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentAccessException; import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentAccessException;
@@ -20,9 +21,6 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionResult; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionResult;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate; import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -79,14 +77,13 @@ import java.util.Objects;
*/ */
public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCase { public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCase {
private static final Logger LOG = LogManager.getLogger(DefaultBatchRunProcessingUseCase.class);
private final StartConfiguration configuration; private final StartConfiguration configuration;
private final RunLockPort runLockPort; private final RunLockPort runLockPort;
private final SourceDocumentCandidatesPort sourceDocumentCandidatesPort; private final SourceDocumentCandidatesPort sourceDocumentCandidatesPort;
private final PdfTextExtractionPort pdfTextExtractionPort; private final PdfTextExtractionPort pdfTextExtractionPort;
private final FingerprintPort fingerprintPort; private final FingerprintPort fingerprintPort;
private final DocumentProcessingCoordinator documentProcessingCoordinator; private final DocumentProcessingCoordinator documentProcessingCoordinator;
private final ProcessingLogger logger;
/** /**
* Creates the batch use case with the already-loaded startup configuration and all * Creates the batch use case with the already-loaded startup configuration and all
@@ -105,6 +102,7 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
* must not be null * must not be null
* @param documentProcessingCoordinator for applying decision logic and persisting results; * @param documentProcessingCoordinator for applying decision logic and persisting results;
* must not be null * must not be null
* @param logger for processing-related logging; must not be null
* @throws NullPointerException if any parameter is null * @throws NullPointerException if any parameter is null
*/ */
public DefaultBatchRunProcessingUseCase( public DefaultBatchRunProcessingUseCase(
@@ -113,7 +111,8 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
SourceDocumentCandidatesPort sourceDocumentCandidatesPort, SourceDocumentCandidatesPort sourceDocumentCandidatesPort,
PdfTextExtractionPort pdfTextExtractionPort, PdfTextExtractionPort pdfTextExtractionPort,
FingerprintPort fingerprintPort, FingerprintPort fingerprintPort,
DocumentProcessingCoordinator documentProcessingCoordinator) { DocumentProcessingCoordinator documentProcessingCoordinator,
ProcessingLogger logger) {
this.configuration = Objects.requireNonNull(configuration, "configuration must not be null"); this.configuration = Objects.requireNonNull(configuration, "configuration must not be null");
this.runLockPort = Objects.requireNonNull(runLockPort, "runLockPort must not be null"); this.runLockPort = Objects.requireNonNull(runLockPort, "runLockPort must not be null");
this.sourceDocumentCandidatesPort = Objects.requireNonNull( this.sourceDocumentCandidatesPort = Objects.requireNonNull(
@@ -123,11 +122,12 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
this.fingerprintPort = Objects.requireNonNull(fingerprintPort, "fingerprintPort must not be null"); this.fingerprintPort = Objects.requireNonNull(fingerprintPort, "fingerprintPort must not be null");
this.documentProcessingCoordinator = Objects.requireNonNull( this.documentProcessingCoordinator = Objects.requireNonNull(
documentProcessingCoordinator, "documentProcessingCoordinator must not be null"); documentProcessingCoordinator, "documentProcessingCoordinator must not be null");
this.logger = Objects.requireNonNull(logger, "logger must not be null");
} }
@Override @Override
public BatchRunOutcome execute(BatchRunContext context) { public BatchRunOutcome execute(BatchRunContext context) {
LOG.info("Batch processing initiated. RunId: {}", context.runId()); logger.info("Batch processing initiated. RunId: {}", context.runId());
boolean lockAcquired = false; boolean lockAcquired = false;
try { try {
@@ -135,23 +135,23 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
try { try {
runLockPort.acquire(); runLockPort.acquire();
lockAcquired = true; lockAcquired = true;
LOG.debug("Run lock acquired successfully."); logger.debug("Run lock acquired successfully.");
} catch (RunLockUnavailableException e) { } catch (RunLockUnavailableException e) {
LOG.warn("Run lock not available another instance is already running. " logger.warn("Run lock not available another instance is already running. "
+ "This instance terminates immediately."); + "This instance terminates immediately.");
return BatchRunOutcome.LOCK_UNAVAILABLE; return BatchRunOutcome.LOCK_UNAVAILABLE;
} }
LOG.debug("Configuration in use: source={}, target={}", logger.debug("Configuration in use: source={}, target={}",
configuration.sourceFolder(), configuration.targetFolder()); configuration.sourceFolder(), configuration.targetFolder());
LOG.info("Batch run started. RunId: {}, Start: {}", logger.info("Batch run started. RunId: {}, Start: {}",
context.runId(), context.startInstant()); context.runId(), context.startInstant());
// Load and process all candidates // Load and process all candidates
return processCandidates(context); return processCandidates(context);
} catch (Exception e) { } catch (Exception e) {
LOG.error("Unexpected error during batch processing", e); logger.error("Unexpected error during batch processing", e);
return BatchRunOutcome.FAILURE; return BatchRunOutcome.FAILURE;
} finally { } finally {
releaseLockIfAcquired(lockAcquired); releaseLockIfAcquired(lockAcquired);
@@ -169,17 +169,17 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
try { try {
candidates = sourceDocumentCandidatesPort.loadCandidates(); candidates = sourceDocumentCandidatesPort.loadCandidates();
} catch (SourceDocumentAccessException e) { } catch (SourceDocumentAccessException e) {
LOG.error("Cannot access source folder: {}", e.getMessage(), e); logger.error("Cannot access source folder: {}", e.getMessage(), e);
return BatchRunOutcome.FAILURE; return BatchRunOutcome.FAILURE;
} }
LOG.info("Found {} PDF candidate(s) in source folder.", candidates.size()); logger.info("Found {} PDF candidate(s) in source folder.", candidates.size());
// Process each candidate // Process each candidate
for (SourceDocumentCandidate candidate : candidates) { for (SourceDocumentCandidate candidate : candidates) {
processCandidate(candidate, context); processCandidate(candidate, context);
} }
LOG.info("Batch run completed. Processed {} candidate(s). RunId: {}", logger.info("Batch run completed. Processed {} candidate(s). RunId: {}",
candidates.size(), context.runId()); candidates.size(), context.runId());
return BatchRunOutcome.SUCCESS; return BatchRunOutcome.SUCCESS;
} }
@@ -196,9 +196,9 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
if (lockAcquired) { if (lockAcquired) {
try { try {
runLockPort.release(); runLockPort.release();
LOG.debug("Run lock released."); logger.debug("Run lock released.");
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Warning: Failed to release run lock.", e); logger.warn("Warning: Failed to release run lock.", e);
} }
} }
} }
@@ -230,7 +230,7 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
* @param context the current batch run context * @param context the current batch run context
*/ */
private void processCandidate(SourceDocumentCandidate candidate, BatchRunContext context) { private void processCandidate(SourceDocumentCandidate candidate, BatchRunContext context) {
LOG.debug("Processing candidate: {}", candidate.uniqueIdentifier()); logger.debug("Processing candidate: {}", candidate.uniqueIdentifier());
Instant attemptStart = Instant.now(); Instant attemptStart = Instant.now();
FingerprintResult fingerprintResult = fingerprintPort.computeFingerprint(candidate); FingerprintResult fingerprintResult = fingerprintPort.computeFingerprint(candidate);
@@ -253,7 +253,7 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
* @param error the fingerprint error * @param error the fingerprint error
*/ */
private void handleFingerprintError(SourceDocumentCandidate candidate, FingerprintTechnicalError error) { private void handleFingerprintError(SourceDocumentCandidate candidate, FingerprintTechnicalError error) {
LOG.warn("Fingerprint computation failed for '{}': {} — candidate skipped (not historised).", logger.warn("Fingerprint computation failed for '{}': {} — candidate skipped (not historised).",
candidate.uniqueIdentifier(), error.errorMessage()); candidate.uniqueIdentifier(), error.errorMessage());
} }
@@ -273,7 +273,7 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
BatchRunContext context, BatchRunContext context,
Instant attemptStart) { Instant attemptStart) {
DocumentFingerprint fingerprint = fingerprintSuccess.fingerprint(); DocumentFingerprint fingerprint = fingerprintSuccess.fingerprint();
LOG.debug("Fingerprint computed for '{}': {}", logger.debug("Fingerprint computed for '{}': {}",
candidate.uniqueIdentifier(), fingerprint.sha256Hex()); candidate.uniqueIdentifier(), fingerprint.sha256Hex());
documentProcessingCoordinator.processDeferredOutcome( documentProcessingCoordinator.processDeferredOutcome(
@@ -317,17 +317,17 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
private void logExtractionResult(SourceDocumentCandidate candidate, PdfExtractionResult result) { private void logExtractionResult(SourceDocumentCandidate candidate, PdfExtractionResult result) {
switch (result) { switch (result) {
case de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess success -> { case de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess success -> {
LOG.debug("PDF extraction successful for '{}'. Pages: {}, Text length: {} chars.", logger.debug("PDF extraction successful for '{}'. Pages: {}, Text length: {} chars.",
candidate.uniqueIdentifier(), candidate.uniqueIdentifier(),
success.pageCount().value(), success.pageCount().value(),
success.extractedText().length()); success.extractedText().length());
} }
case de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionContentError contentError -> { case de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionContentError contentError -> {
LOG.debug("PDF content extraction failed for '{}' (content problem): {}", logger.debug("PDF content extraction failed for '{}' (content problem): {}",
candidate.uniqueIdentifier(), contentError.reason()); candidate.uniqueIdentifier(), contentError.reason());
} }
case de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionTechnicalError technicalError -> { case de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionTechnicalError technicalError -> {
LOG.debug("PDF extraction technical error for '{}': {}", logger.debug("PDF extraction technical error for '{}': {}",
candidate.uniqueIdentifier(), technicalError.errorMessage()); candidate.uniqueIdentifier(), technicalError.errorMessage());
} }
default -> { default -> {
@@ -345,15 +345,15 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
private void logProcessingOutcome(SourceDocumentCandidate candidate, DocumentProcessingOutcome outcome) { private void logProcessingOutcome(SourceDocumentCandidate candidate, DocumentProcessingOutcome outcome) {
switch (outcome) { switch (outcome) {
case de.gecheckt.pdf.umbenenner.domain.model.PreCheckPassed passed -> { case de.gecheckt.pdf.umbenenner.domain.model.PreCheckPassed passed -> {
LOG.info("Pre-checks PASSED for '{}'. Candidate ready for persistence.", logger.info("Pre-checks PASSED for '{}'. Candidate ready for persistence.",
candidate.uniqueIdentifier()); candidate.uniqueIdentifier());
} }
case de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed failed -> { case de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed failed -> {
LOG.info("Pre-checks FAILED for '{}': {} (Deterministic content error).", logger.info("Pre-checks FAILED for '{}': {} (Deterministic content error).",
candidate.uniqueIdentifier(), failed.failureReasonDescription()); candidate.uniqueIdentifier(), failed.failureReasonDescription());
} }
case de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError technicalError -> { case de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError technicalError -> {
LOG.warn("Processing FAILED for '{}': {} (Technical error retryable).", logger.warn("Processing FAILED for '{}': {} (Technical error retryable).",
candidate.uniqueIdentifier(), technicalError.errorMessage()); candidate.uniqueIdentifier(), technicalError.errorMessage());
} }
default -> { default -> {

View File

@@ -12,6 +12,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceLookupTechnicalFailure; import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceLookupTechnicalFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort; import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext; import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint; import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
@@ -71,7 +72,7 @@ class DocumentProcessingCoordinatorTest {
recordRepo = new CapturingDocumentRecordRepository(); recordRepo = new CapturingDocumentRecordRepository();
attemptRepo = new CapturingProcessingAttemptRepository(); attemptRepo = new CapturingProcessingAttemptRepository();
unitOfWorkPort = new CapturingUnitOfWorkPort(recordRepo, attemptRepo); unitOfWorkPort = new CapturingUnitOfWorkPort(recordRepo, attemptRepo);
processor = new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort); processor = new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort, new NoOpProcessingLogger());
candidate = new SourceDocumentCandidate( candidate = new SourceDocumentCandidate(
"test.pdf", 1024L, new SourceDocumentLocator("/tmp/test.pdf")); "test.pdf", 1024L, new SourceDocumentLocator("/tmp/test.pdf"));
@@ -467,4 +468,26 @@ class DocumentProcessingCoordinatorTest {
operations.accept(mockOps); operations.accept(mockOps);
} }
} }
private static class NoOpProcessingLogger implements ProcessingLogger {
@Override
public void info(String message, Object... args) {
// No-op
}
@Override
public void debug(String message, Object... args) {
// No-op
}
@Override
public void warn(String message, Object... args) {
// No-op
}
@Override
public void error(String message, Object... args) {
// No-op
}
}
} }

View File

@@ -13,6 +13,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintTechnicalError
import de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort; import de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException;
import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentAccessException; import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentAccessException;
@@ -435,7 +436,8 @@ class BatchRunProcessingUseCaseTest {
FingerprintPort fingerprintPort, FingerprintPort fingerprintPort,
DocumentProcessingCoordinator processor) { DocumentProcessingCoordinator processor) {
return new DefaultBatchRunProcessingUseCase( return new DefaultBatchRunProcessingUseCase(
config, lockPort, candidatesPort, extractionPort, fingerprintPort, processor); config, lockPort, candidatesPort, extractionPort, fingerprintPort, processor,
new NoOpProcessingLogger());
} }
private static StartConfiguration buildConfig(Path tempDir) throws Exception { private static StartConfiguration buildConfig(Path tempDir) throws Exception {
@@ -617,7 +619,8 @@ class BatchRunProcessingUseCaseTest {
*/ */
private static class NoOpDocumentProcessingCoordinator extends DocumentProcessingCoordinator { private static class NoOpDocumentProcessingCoordinator extends DocumentProcessingCoordinator {
NoOpDocumentProcessingCoordinator() { NoOpDocumentProcessingCoordinator() {
super(new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(), new NoOpUnitOfWorkPort()); super(new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(), new NoOpUnitOfWorkPort(),
new NoOpProcessingLogger());
} }
} }
@@ -628,7 +631,8 @@ class BatchRunProcessingUseCaseTest {
private int processCallCount = 0; private int processCallCount = 0;
TrackingDocumentProcessingCoordinator() { TrackingDocumentProcessingCoordinator() {
super(new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(), new NoOpUnitOfWorkPort()); super(new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(), new NoOpUnitOfWorkPort(),
new NoOpProcessingLogger());
} }
@Override @Override
@@ -705,12 +709,12 @@ class BatchRunProcessingUseCaseTest {
public void saveProcessingAttempt(ProcessingAttempt attempt) { public void saveProcessingAttempt(ProcessingAttempt attempt) {
// No-op // No-op
} }
@Override @Override
public void createDocumentRecord(DocumentRecord record) { public void createDocumentRecord(DocumentRecord record) {
// No-op // No-op
} }
@Override @Override
public void updateDocumentRecord(DocumentRecord record) { public void updateDocumentRecord(DocumentRecord record) {
// No-op // No-op
@@ -718,4 +722,27 @@ class BatchRunProcessingUseCaseTest {
}); });
} }
} }
/** No-op ProcessingLogger for use in test instances. */
private static class NoOpProcessingLogger implements ProcessingLogger {
@Override
public void info(String message, Object... args) {
// No-op
}
@Override
public void debug(String message, Object... args) {
// No-op
}
@Override
public void warn(String message, Object... args) {
// No-op
}
@Override
public void error(String message, Object... args) {
// No-op
}
}
} }

View File

@@ -29,10 +29,12 @@ import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintPort; import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintPort;
import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort; import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort; import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
import de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingCoordinator; import de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingCoordinator;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultBatchRunProcessingUseCase; import de.gecheckt.pdf.umbenenner.application.usecase.DefaultBatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.bootstrap.adapter.Log4jProcessingLogger;
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext; import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
import de.gecheckt.pdf.umbenenner.domain.model.RunId; import de.gecheckt.pdf.umbenenner.domain.model.RunId;
@@ -171,15 +173,17 @@ public class BootstrapRunner {
new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl); new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl);
UnitOfWorkPort unitOfWorkPort = UnitOfWorkPort unitOfWorkPort =
new SqliteUnitOfWorkAdapter(jdbcUrl); new SqliteUnitOfWorkAdapter(jdbcUrl);
ProcessingLogger logger = new Log4jProcessingLogger(DefaultBatchRunProcessingUseCase.class);
DocumentProcessingCoordinator documentProcessingCoordinator = DocumentProcessingCoordinator documentProcessingCoordinator =
new DocumentProcessingCoordinator(documentRecordRepository, processingAttemptRepository, unitOfWorkPort); new DocumentProcessingCoordinator(documentRecordRepository, processingAttemptRepository, unitOfWorkPort, logger);
return new DefaultBatchRunProcessingUseCase( return new DefaultBatchRunProcessingUseCase(
config, config,
lock, lock,
new SourceDocumentCandidatesPortAdapter(config.sourceFolder()), new SourceDocumentCandidatesPortAdapter(config.sourceFolder()),
new PdfTextExtractionPortAdapter(), new PdfTextExtractionPortAdapter(),
fingerprintPort, fingerprintPort,
documentProcessingCoordinator); documentProcessingCoordinator,
logger);
}; };
this.commandFactory = SchedulerBatchCommand::new; this.commandFactory = SchedulerBatchCommand::new;
} }

View File

@@ -0,0 +1,57 @@
package de.gecheckt.pdf.umbenenner.bootstrap.adapter;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Log4j-based adapter implementing the {@link ProcessingLogger} port.
* <p>
* This adapter bridges the application layer to the concrete Log4j2 framework,
* allowing the application to remain decoupled from logging technology choices.
* <p>
* The error method intelligently detects if the last argument is a Throwable
* and logs accordingly.
*/
public class Log4jProcessingLogger implements ProcessingLogger {
private final Logger log4jLogger;
/**
* Creates a logger instance for the given class.
*
* @param clazz the class to derive the logger name from; must not be null
*/
public Log4jProcessingLogger(Class<?> clazz) {
this.log4jLogger = LogManager.getLogger(clazz);
}
@Override
public void info(String message, Object... args) {
log4jLogger.info(message, args);
}
@Override
public void debug(String message, Object... args) {
log4jLogger.debug(message, args);
}
@Override
public void warn(String message, Object... args) {
log4jLogger.warn(message, args);
}
@Override
public void error(String message, Object... args) {
// If the last argument is a Throwable, extract it and pass separately
if (args.length > 0 && args[args.length - 1] instanceof Throwable throwable) {
Object[] messageArgs = new Object[args.length - 1];
System.arraycopy(args, 0, messageArgs, 0, args.length - 1);
log4jLogger.error(message, throwable, messageArgs);
} else {
log4jLogger.error(message, args);
}
}
}