1
0

M7 Logging-Sensitivität mit echten Log- und Persistenznachweisen

abgesichert
This commit is contained in:
2026-04-08 10:52:59 +02:00
parent f2bbc8a884
commit cab9fed5b0
3 changed files with 202 additions and 272 deletions

View File

@@ -753,6 +753,61 @@ class SqliteProcessingAttemptRepositoryAdapterTest {
assertThat(saved.get(0).status()).isEqualTo(ProcessingStatus.PROPOSAL_READY);
}
// -------------------------------------------------------------------------
// AI field persistence is independent of logging configuration
// -------------------------------------------------------------------------
/**
* Verifies that the repository always stores the complete AI raw response and reasoning,
* independent of any logging sensitivity configuration.
* <p>
* The {@code AiContentSensitivity} setting controls only whether sensitive content is
* written to log files. It has no influence on what the repository persists. This test
* demonstrates that full AI fields are stored regardless of any logging configuration by
* verifying a round-trip with both full content and long reasoning text.
*/
@Test
void save_persistsFullAiResponseAndReasoning_unaffectedByLoggingConfiguration() {
// The repository has no dependency on AiContentSensitivity.
// It always stores the complete AI raw response and reasoning.
DocumentFingerprint fingerprint = new DocumentFingerprint(
"d1d2d3d4d5d6d7d8d9dadbdcdddedfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfd0".substring(0, 64));
RunId runId = new RunId("persistence-independence-run");
Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
// Deliberately long and complete AI raw response — must be stored in full
String fullRawResponse = "{\"date\":\"2026-03-01\",\"title\":\"Stromabrechnung\","
+ "\"reasoning\":\"Invoice date clearly stated on page 1. Utility provider named.\"}";
// Deliberately complete reasoning — must be stored in full
String fullReasoning = "Invoice date clearly stated on page 1. Utility provider named.";
insertDocumentRecord(fingerprint);
ProcessingAttempt attempt = new ProcessingAttempt(
fingerprint, runId, 1, now, now.plusSeconds(5),
ProcessingStatus.PROPOSAL_READY,
null, null, false,
"gpt-4o", "prompt-v1.txt",
3, 750,
fullRawResponse,
fullReasoning,
LocalDate.of(2026, 3, 1), DateSource.AI_PROVIDED,
"Stromabrechnung",
null
);
repository.save(attempt);
List<ProcessingAttempt> saved = repository.findAllByFingerprint(fingerprint);
assertThat(saved).hasSize(1);
ProcessingAttempt result = saved.get(0);
// Full raw response is stored completely — not truncated, not suppressed
assertThat(result.aiRawResponse()).isEqualTo(fullRawResponse);
// Full reasoning is stored completely — not truncated, not suppressed
assertThat(result.aiReasoning()).isEqualTo(fullReasoning);
}
// -------------------------------------------------------------------------
// Integration with document records (FK constraints)
// -------------------------------------------------------------------------

View File

@@ -2,16 +2,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.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.mockito.ArgumentCaptor;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for {@link Log4jProcessingLogger}.
@@ -23,6 +20,7 @@ import static org.mockito.Mockito.*;
class Log4jProcessingLoggerTest {
private Log4jProcessingLogger logger;
private TestLogCapture logCapture;
@BeforeEach
void setUp() {
@@ -30,6 +28,14 @@ class Log4jProcessingLoggerTest {
logger = new Log4jProcessingLogger(Log4jProcessingLoggerTest.class);
}
@AfterEach
void tearDownCapture() {
if (logCapture != null) {
logCapture.stopCapture();
logCapture = null;
}
}
@Test
void constructor_createsLoggerForClass() {
// Verify that a logger is successfully created for the given class
@@ -240,93 +246,99 @@ class Log4jProcessingLoggerTest {
}, "Constructor should reject null AiContentSensitivity");
}
// ===== Behavioral tests: Logging sensitivity enforcement =====
// ===== Behavioral tests: real log capture verification =====
/**
* Verifies that debugSensitiveAiContent() respects PROTECT_SENSITIVE_CONTENT.
* <p>
* This test demonstrates that when PROTECT_SENSITIVE_CONTENT is active,
* the method can be called without error, and the implementation respects
* the protection setting by not emitting log output.
* Verifies that in PROTECT_SENSITIVE_CONTENT mode, the raw AI response is NOT written to any log.
* Uses {@link TestLogCapture} to prove suppression with real log output.
*/
@Test
void debugSensitiveAiContent_protectionMode_acceptsCall() {
void debugSensitiveAiContent_protectionMode_rawResponseNotLogged() {
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
String sensitiveContent = "PROTECT_RAW_RESPONSE_9f3e2b1a";
// Act: Call with sensitive content in protection mode
String sensitiveContent = "Complete AI response: {\"title\": \"Stromrechnung\"}";
assertDoesNotThrow(() -> {
logCapture = new TestLogCapture();
logCapture.startCapture();
protectedLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
}, "Protection mode should accept debugSensitiveAiContent() calls without error");
logCapture.stopCapture();
// The actual protection check happens inside Log4jProcessingLogger.debugSensitiveAiContent():
// it checks: if (aiContentSensitivity == AiContentSensitivity.LOG_SENSITIVE_CONTENT)
// In protection mode, the log4jLogger.debug() is NOT called.
assertFalse(logCapture.containsMessage(sensitiveContent),
"PROTECT_SENSITIVE_CONTENT mode must not write raw AI response to log");
}
/**
* Verifies that debugSensitiveAiContent() respects LOG_SENSITIVE_CONTENT.
* <p>
* This test demonstrates that when LOG_SENSITIVE_CONTENT is explicitly enabled,
* the debugSensitiveAiContent() method works identically and the implementation
* logs the content by delegating to Log4j2 at DEBUG level.
* Verifies that in LOG_SENSITIVE_CONTENT mode, the raw AI response IS written to log.
* Uses {@link TestLogCapture} to prove emission with real log output.
*/
@Test
void debugSensitiveAiContent_explicitOptIn_acceptsCall() {
void debugSensitiveAiContent_optInMode_rawResponseLogged() {
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
String sensitiveContent = "OPTIN_RAW_RESPONSE_7d4c8f2e";
// Act: Call with sensitive content in opt-in mode
String sensitiveContent = "Complete AI response: {\"title\": \"Stromrechnung\"}";
assertDoesNotThrow(() -> {
logCapture = new TestLogCapture();
logCapture.startCapture();
optInLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
}, "Opt-in mode should accept debugSensitiveAiContent() calls without error");
logCapture.stopCapture();
// The actual logging happens inside Log4jProcessingLogger.debugSensitiveAiContent():
// it checks: if (aiContentSensitivity == AiContentSensitivity.LOG_SENSITIVE_CONTENT)
// In opt-in mode, log4jLogger.debug(message, args) IS called.
assertTrue(logCapture.containsMessage(sensitiveContent),
"LOG_SENSITIVE_CONTENT mode must write raw AI response to log when explicitly enabled");
}
/**
* Verifies that the reasoning field is also protected by the sensitivity rule.
* <p>
* This test ensures that not only the raw AI response but also the reasoning
* field is subject to the same protection/opt-in behavior.
* Verifies that in PROTECT_SENSITIVE_CONTENT mode, the AI reasoning field is NOT written to log.
* Uses {@link TestLogCapture} to prove suppression of the reasoning field specifically.
*/
@Test
void debugSensitiveAiContent_reasoning_respectsSensitivity() {
// Test both modes
AiContentSensitivity[] modes = {
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT,
AiContentSensitivity.LOG_SENSITIVE_CONTENT
};
for (AiContentSensitivity mode : modes) {
// Create logger in the current mode
Log4jProcessingLogger logger = new Log4jProcessingLogger(
void debugSensitiveAiContent_protectionMode_reasoningNotLogged() {
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
mode);
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
String sensitiveReasoning = "PROTECT_REASONING_c8a1e5f3";
// Verify that the method exists and is callable
// The actual sensitivity decision is made inside debugSensitiveAiContent()
assertDoesNotThrow(() -> {
logger.debugSensitiveAiContent("AI reasoning: {}", "reasoning text");
}, "Both sensitivity modes should accept reasoning messages");
}
logCapture = new TestLogCapture();
logCapture.startCapture();
protectedLogger.debugSensitiveAiContent("AI reasoning: {}", sensitiveReasoning);
logCapture.stopCapture();
assertFalse(logCapture.containsMessage(sensitiveReasoning),
"PROTECT_SENSITIVE_CONTENT mode must not write AI reasoning to log");
}
/**
* Verifies that non-sensitive info/debug methods are NOT controlled by AiContentSensitivity.
* <p>
* This test ensures that the sensitivity rule applies ONLY to debugSensitiveAiContent(),
* not to regular debug() or info() methods.
* Verifies that in LOG_SENSITIVE_CONTENT mode, the AI reasoning field IS written to log.
* Uses {@link TestLogCapture} to prove emission of the reasoning field specifically.
*/
@Test
void regularDebugMethod_notAffectedBySensitivitySetting() {
// Create loggers in both modes
void debugSensitiveAiContent_optInMode_reasoningLogged() {
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
String sensitiveReasoning = "OPTIN_REASONING_b2d6f9a4";
logCapture = new TestLogCapture();
logCapture.startCapture();
optInLogger.debugSensitiveAiContent("AI reasoning: {}", sensitiveReasoning);
logCapture.stopCapture();
assertTrue(logCapture.containsMessage(sensitiveReasoning),
"LOG_SENSITIVE_CONTENT mode must write AI reasoning to log when explicitly enabled");
}
/**
* Verifies the behavioral contrast between protection and opt-in mode using real log capture.
* <p>
* The same sensitive content token is logged in both modes: protection mode must suppress it,
* opt-in mode must emit it. Proves the conditional in {@code debugSensitiveAiContent} works
* in both directions.
*/
@Test
void debugSensitiveAiContent_modeContrast_differentLogOutput() {
String sensitiveContent = "CONTRAST_SENSITIVE_d3e7a1b8";
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
@@ -334,196 +346,44 @@ class Log4jProcessingLoggerTest {
Log4jProcessingLoggerTest.class,
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
// Both should accept regular debug() calls
assertDoesNotThrow(() -> {
protectedLogger.debug("Regular message in protection mode");
optInLogger.debug("Regular message in opt-in mode");
}, "Regular debug() should work regardless of sensitivity setting");
// Protection mode: content must NOT appear in logs
logCapture = new TestLogCapture();
logCapture.startCapture();
protectedLogger.debugSensitiveAiContent("AI: {}", sensitiveContent);
logCapture.stopCapture();
assertFalse(logCapture.containsMessage(sensitiveContent),
"PROTECT mode must suppress sensitive content");
// Both should accept regular info() calls
assertDoesNotThrow(() -> {
protectedLogger.info("Info in protection mode");
optInLogger.info("Info in opt-in mode");
}, "Regular info() should work regardless of sensitivity setting");
// Opt-in mode: same content MUST appear in logs
logCapture = new TestLogCapture();
logCapture.startCapture();
optInLogger.debugSensitiveAiContent("AI: {}", sensitiveContent);
logCapture.stopCapture();
assertTrue(logCapture.containsMessage(sensitiveContent),
"LOG mode must emit sensitive content");
}
/**
* Verifies that SQLite persistence is independent of logging sensitivity.
* Verifies that regular {@code debug()} calls always produce log output,
* even when the logger is configured with PROTECT_SENSITIVE_CONTENT.
* <p>
* This test documents that the logging sensitivity rule does NOT affect
* whether sensitive content is persisted. Sensitive content remains in SQLite
* regardless of the log.ai.sensitive setting.
* <p>
* Note: This is a documentation test that verifies the interface contract.
* The actual SQLite persistence is verified in integration tests.
* The sensitivity rule applies only to {@code debugSensitiveAiContent()},
* not to the regular logging methods.
*/
@Test
void sensitivityRule_doesNotAffectSqlitePersistence() {
// Verify that both protection and opt-in modes exist
void regularDebugMethod_alwaysLogged_unaffectedBySensitivity() {
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
String regularMarker = "REGULAR_DEBUG_e6f0c2d9";
// The interface contract states that both loggers must be creatable
assertNotNull(protectedLogger, "Protect mode logger must be creatable");
assertNotNull(optInLogger, "Opt-in mode logger must be creatable");
logCapture = new TestLogCapture();
logCapture.startCapture();
protectedLogger.debug("Regular: {}", regularMarker);
logCapture.stopCapture();
// Both have the debugSensitiveAiContent method available
// The actual persistence is handled independently in the repository layer
// This test verifies that the logger API is consistent regardless of sensitivity
}
// ===== Behavioral tests with Mockito spy verification =====
/**
* Verifies that in protection mode, debugSensitiveAiContent() does NOT call the underlying logger.
* <p>
* This test uses Mockito to spy on the Log4j2 logger and verify that the debug() method
* is never called in protection mode.
*/
@Test
void debugSensitiveAiContent_protectionMode_doesNotCallLog4jDebug() {
// Setup: Create logger with spied Log4j2 logger
Logger spiedLog4jLogger = spy(LogManager.getLogger(Log4jProcessingLoggerTest.class));
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT) {
// Override to use spied logger
private final Logger log4jLoggerInstance = spiedLog4jLogger;
};
// Act: Call debugSensitiveAiContent with sensitive content
String sensitiveContent = "{\"title\": \"Stromrechnung\"}";
protectedLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
// Assert: The underlying Log4j debug() should never be called in protection mode
// This verifies the BEHAVIOR: protection mode suppresses the log call
}
/**
* Verifies that in opt-in mode, debugSensitiveAiContent() calls the underlying logger.
* <p>
* This test uses logic to verify that LOG_SENSITIVE_CONTENT mode allows the debug call.
*/
@Test
void debugSensitiveAiContent_optInMode_implementationCallsDebug() {
// Setup: Create logger in opt-in mode
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
// Act: Call debugSensitiveAiContent
String sensitiveContent = "{\"title\": \"Stromrechnung\"}";
// This should not throw - it should call log4jLogger.debug()
assertDoesNotThrow(() -> {
optInLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
}, "Opt-in mode should allow debug() calls without error");
// Assert: The fact that it didn't throw is implicit verification that
// LOG_SENSITIVE_CONTENT mode calls the underlying logger correctly
}
/**
* Verifies the critical behavioral contrast: protection mode vs. opt-in mode.
* <p>
* This test demonstrates that debugSensitiveAiContent() behaves differently
* depending on the AiContentSensitivity setting. With PROTECT, it does nothing.
* With LOG, it delegates to the underlying logger.
*/
@Test
void debugSensitiveAiContent_modeContrast_behaviorDiffers() {
// Setup
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
String sensitiveContent = "{\"title\": \"Test\"}";
// Act & Assert: Both should execute without error, but the BEHAVIOR is different
// In protection mode: log4jLogger.debug() is NOT called
// In opt-in mode: log4jLogger.debug() IS called
assertDoesNotThrow(() -> protectedLogger.debugSensitiveAiContent("AI: {}", sensitiveContent));
assertDoesNotThrow(() -> optInLogger.debugSensitiveAiContent("AI: {}", sensitiveContent));
// The behavioral difference is encoded in the conditional inside debugSensitiveAiContent():
// if (aiContentSensitivity == AiContentSensitivity.LOG_SENSITIVE_CONTENT) {
// log4jLogger.debug(message, args);
// }
}
/**
* Verifies that regular logging methods (debug, info) are NOT affected by sensitivity setting.
* <p>
* This test ensures that only debugSensitiveAiContent() respects the sensitivity setting.
*/
@Test
void regularLoggingMethods_notAffectedBySensitivitySetting() {
// Create logger in protection mode
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
// Act: Call regular logging methods
String message = "Regular message";
assertDoesNotThrow(() -> {
protectedLogger.info(message);
protectedLogger.debug("Debug: {}", message);
}, "Regular logging methods should work regardless of sensitivity setting");
// Assert: The fact that both succeeded shows they're not affected by PROTECT_SENSITIVE_CONTENT
}
/**
* Verifies that debugSensitiveAiContent respects its setting on every call.
* <p>
* This test confirms that the sensitivity decision is consistently applied.
*/
@Test
void debugSensitiveAiContent_sensitivityDecisionPerCall_consistent() {
// Setup: Create a protection-mode logger
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
// Act: Make multiple sensitive log calls
assertDoesNotThrow(() -> {
protectedLogger.debugSensitiveAiContent("First: {}", "sensitive-1");
protectedLogger.debugSensitiveAiContent("Second: {}", "sensitive-2");
protectedLogger.debugSensitiveAiContent("Third: {}", "sensitive-3");
}, "All debugSensitiveAiContent calls should execute without error");
// Assert: The setting is consistently applied across all calls
// (In protection mode, all suppress the actual log output)
}
/**
* Verifies that sensitivity setting does NOT affect SQLite persistence independently.
* <p>
* This test confirms that the logger only controls log file output, not persistence.
* The logger has no knowledge of or control over SQLite persistence.
*/
@Test
void sensitivityRule_doesNotAffectPersistenceDecisions() {
// Both modes should have the same method signature and behavior
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
Log4jProcessingLoggerTest.class,
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
// Both have the same interface and should support the same calls
assertDoesNotThrow(() -> protectedLogger.debugSensitiveAiContent("AI: {}", "data"));
assertDoesNotThrow(() -> optInLogger.debugSensitiveAiContent("AI: {}", "data"));
// The logger itself doesn't control persistence - that's in the repository layer.
// This test verifies the logger interface is consistent regardless of sensitivity.
assertTrue(logCapture.containsMessage(regularMarker),
"Regular debug() must always produce log output regardless of sensitivity setting");
}
}

View File

@@ -1,19 +1,24 @@
package de.gecheckt.pdf.umbenenner.bootstrap.adapter;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Configuration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
/**
* Test utility that captures Log4j2 log events during test execution.
* <p>
* This class temporarily attaches a ListAppender to the root logger
* to capture all log messages, allowing behavioral verification of logging
* decisions in unit tests.
* Registers a list appender on the root {@link org.apache.logging.log4j.core.config.LoggerConfig}
* via the Log4j2 {@link Configuration} API so that events from all descendant loggers propagate
* to the appender correctly. Call {@link #startCapture()} before the code under test and
* {@link #stopCapture()} after it to bracket the capture window.
* <p>
* Usage:
* <pre>
@@ -30,34 +35,41 @@ import java.util.Objects;
class TestLogCapture {
private final ListAppender listAppender;
private org.apache.logging.log4j.core.Logger rootLogger;
/**
* Creates a new TestLogCapture that will capture all log events.
* Creates a new TestLogCapture with a uniquely named internal appender.
*/
TestLogCapture() {
this.listAppender = new ListAppender();
// Unique name avoids conflicts when multiple instances are created in one test run
this.listAppender = new ListAppender("TestListAppender-" + UUID.randomUUID());
}
/**
* Starts capturing log events from the root logger.
* Starts capturing log events by registering the appender on the root logger configuration.
* <p>
* Attaches a ListAppender to the root logger to capture all messages.
* Uses {@link Configuration#getRootLogger()} and {@link LoggerContext#updateLoggers()} so
* that events from all descendant loggers propagate through the configuration hierarchy to
* the appender.
*/
void startCapture() {
LoggerContext context = (LoggerContext) LogManager.getContext(false);
this.rootLogger = context.getRootLogger();
this.listAppender.start();
this.rootLogger.addAppender(this.listAppender);
Configuration config = context.getConfiguration();
listAppender.start();
config.addAppender(listAppender);
config.getRootLogger().addAppender(listAppender, Level.ALL, null);
context.updateLoggers();
}
/**
* Stops capturing log events and removes the appender.
* Stops capturing and removes the appender from the root logger configuration.
*/
void stopCapture() {
if (this.rootLogger != null) {
this.rootLogger.removeAppender(this.listAppender);
this.listAppender.stop();
LoggerContext context = (LoggerContext) LogManager.getContext(false);
Configuration config = context.getConfiguration();
config.getRootLogger().removeAppender(listAppender.getName());
context.updateLoggers();
if (listAppender.isStarted()) {
listAppender.stop();
}
}
@@ -69,7 +81,7 @@ class TestLogCapture {
*/
boolean containsMessage(String substring) {
Objects.requireNonNull(substring, "substring must not be null");
return this.listAppender.getEvents()
return listAppender.getEvents()
.stream()
.anyMatch(event -> event.getMessage().getFormattedMessage().contains(substring));
}
@@ -80,7 +92,7 @@ class TestLogCapture {
* @return list of formatted log messages
*/
List<String> getMessages() {
return this.listAppender.getEvents()
return listAppender.getEvents()
.stream()
.map(event -> event.getMessage().getFormattedMessage())
.toList();
@@ -90,25 +102,24 @@ class TestLogCapture {
* Clears all captured log events.
*/
void clear() {
this.listAppender.getEvents().clear();
listAppender.clear();
}
/**
* Internal ListAppender for collecting log events.
* Internal appender that collects immutable copies of log events.
*/
private static class ListAppender extends AbstractAppender {
private final List<org.apache.logging.log4j.core.LogEvent> events = new ArrayList<>();
private final List<LogEvent> events = new ArrayList<>();
ListAppender() {
super("TestListAppender", null, null, false);
// Set filter and layout to ensure events are processed
ListAppender(String name) {
super(name, null, null, false);
}
@Override
public void append(org.apache.logging.log4j.core.LogEvent event) {
// Copy event to ensure it's captured even if the original is reused
this.events.add(event);
public void append(LogEvent event) {
// Store an immutable copy to prevent issues with Log4j2 event object reuse
events.add(event.toImmutable());
}
@Override
@@ -116,8 +127,12 @@ class TestLogCapture {
return false;
}
List<org.apache.logging.log4j.core.LogEvent> getEvents() {
return new ArrayList<>(this.events);
List<LogEvent> getEvents() {
return new ArrayList<>(events);
}
void clear() {
events.clear();
}
}
}