Skip to content

Repository files navigation

log-assert

Warning

Experimental. This library is under active development. APIs may change between minor versions without notice. Not recommended for production test suites yet — use at your own risk and pin to an exact version.

log-assert logo

CIMaven CentralDownloadsLicense

In-memory log capture with AssertJ-style assertions for JUnit 5.

Designed for Quarkus and any JBoss Log Manager environment.
SLF4J API + JUnit 5 extension + fluent AssertJ DSL.


Why log-assert?

Other approaches to log assertion in the JBoss/Quarkus ecosystem:

ApproachProblem
Logback ListAppenderVerbose setup per test; no DSL; does not work with JBoss Log Manager
Manual java.util.logging.HandlerBoilerplate; you write all the assertion logic yourself
Quarkus built-in log captureQuarkus-only; no standalone Java support; no fluent DSL

log-assert is the only library offering a fluent AssertJ-style DSL over JBoss Log Manager events with full Quarkus lifecycle awareness — works in plain JUnit 5 tests and @QuarkusTest alike.


Compatibility

DependencyMinimum version
Java21
JUnit 5 (junit-jupiter)5.10+
AssertJ3.24+
JBoss Log Manager3.0+
SLF4J2.0+
Quarkus (optional)3.x

Quick start

Maven dependency

<dependency>
<groupId>io.github.usmanakram232</groupId>
<artifactId>log-assert</artifactId>
<version>1.0.2</version>
<scope>test</scope>
</dependency>

Minimal example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
classOrderServiceTest {
@InjectLogCaptorLogCaptorlogCaptor; // auto-injected and auto-cleared before each test@Testvoidpayment_failure_is_logged() {
orderService.processPayment(invalidRequest);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.fromLogger(OrderService.class)
.hasSize(1)
.single()
.hasFormattedMessageContaining("Payment rejected")
.hasThrowable(IllegalArgumentException.class);
}
}

Quarkus setup

Quarkus re-initialises the JBoss Log Manager after the extension class is loaded, which would uninstall any handler registered via @ExtendWith. Use the @RegisterExtension static form so the extension runs after Quarkus has finished resetting the log manager:

importio.quarkus.test.junit.QuarkusTest;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.extension.RegisterExtension;
@QuarkusTestclassPaymentResourceTest {
@RegisterExtension// MUST be staticstaticLogCaptorExtensionlogExt = LogCaptorExtension.create();
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidcaptures_payment_error() {
// ... trigger code under test ...assertThat(logCaptor)
.atLevel(Level.ERROR)
.isNotEmpty();
}
}

Why static? JUnit 5's @RegisterExtension on a static field registers the extension as a class-level extension (like @BeforeAll/@AfterAll), giving it a chance to install the capture handler after the Quarkus test machinery has finished setting up the log manager. A non-static field would register it as an instance-level extension, which runs too early.


Annotations

@InjectLogCaptor

Marks a LogCaptor field on a test class for automatic injection by LogCaptorExtension. The field is set (and the captor is cleared) before each test method.

@InjectLogCaptorLogCaptorcaptor;

The captor can also be received as a method parameter:

@Testvoidexample(LogCaptorcaptor) { ... }

@PrintLogsOnFailure

When present on a test class or method, dumps all captured log entries to System.err when that test fails. Useful for diagnosing flaky or unexpected failures without permanently enabling verbose logging.

@ExtendWith(LogCaptorExtension.class)
@PrintLogsOnFailure// dump logs whenever any test in this class failsclassMyTest { ... }

Can also be placed on a single method:

@Test@PrintLogsOnFailure// dump logs only when this test failsvoidnoisy_integration_test() { ... }

@EchoLogs

Re-enables console log output for the annotated test scope. By default, LogCaptorExtension detaches all console/stream handlers from the root logger to prevent test noise. Annotating with @EchoLogs signals that log output should still be forwarded to the console for that scope.

@EchoLogs(minimumLevel = Level.DEBUG) // echo DEBUG and above to console during this test@Testvoiddebug_heavy_test() { ... }
AttributeDefaultDescription
minimumLevelTRACEMinimum SLF4J level to echo.

@FailOnUncheckedError

Causes the test to fail in afterEach if any ERROR-level log entries remain in the captor that were not asserted on. This is a safety net to catch unexpected error logging.

@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// any un-asserted ERROR entry fails the testclassStrictServiceTest { ... }

The check fires in afterEachonly when the test itself passed. If the test body already threw an exception or AssertionError, @FailOnUncheckedError is suppressed to avoid masking the original failure. If you asserted ERROR entries in the test body, call logCaptor.clearLogs() after your assertions to prevent a spurious @FailOnUncheckedError failure.


Assertion API reference

Entry point

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;

LogCaptorAssertions is deliberately named to avoid clashing with AssertJ's own org.assertj.core.api.Assertions when both are statically imported in the same class.

// From a LogCaptor (snapshot taken at call time)assertThat(logCaptor) ...
// From a pre-fetched listList<LogEntry> entries = logCaptor.getLogs();
assertThat(entries) ...

LogsAssert — filters (chainable, return new LogsAssert)

MethodDescription
atLevel(Level level)Keep only entries at exactly the given SLF4J level.
atLevelAtLeast(Level minimum)Keep only entries at minimum severity or higher.
fromLogger(Class<?> clazz)Keep only entries whose logger name starts with clazz.getName().
fromLogger(String prefix)Keep only entries whose logger name starts with prefix.
containingMessage(String substring)Keep only entries whose formatted message contains substring (case-sensitive).
withMdcEntry(String key, String value)Keep only entries whose MDC context has key mapped to value.

Filters return a newLogsAssert over the narrowed list and do not mutate the original.

LogsAssert — terminal assertions

MethodDescription
hasSize(int n)Assert exactly n entries. Failure message lists all captured entries.
isEmpty()Assert no entries captured.
isNotEmpty()Assert at least one entry captured.

LogsAssert — navigation (return LogEntryAssert)

MethodDescription
single()Assert exactly one entry, then return an assertion for it.
first()Assert at least one entry, then return an assertion for the first.
last()Assert at least one entry, then return an assertion for the last.

LogEntryAssert — single-entry assertions

Message

MethodDescription
hasFormattedMessage(String expected)Assert the fully-resolved message equals expected.
hasFormattedMessageContaining(String substring)Assert the resolved message contains substring.
hasFormattedMessageMatching(Pattern regex)Assert the resolved message matches the regex (uses find(), not matches()).
hasRawTemplate(String expected)Assert the raw SLF4J template before {} substitution.

Bridge note: when using slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the fully resolved string. Use hasFormattedMessageContaining in all environments.

Level & logger

MethodDescription
hasLevel(Level level)Assert the log level exactly.
hasLoggerName(String name)Assert the fully-qualified logger name exactly.

MDC

MethodDescription
hasMdcEntry(String key, String value)Assert the MDC context at log time contained key → value.

Throwable

MethodDescription
hasThrowable(Class<? extends Throwable> type)Assert a throwable was captured and its class equals type.
hasThrowableWithMessage(String message)Assert a throwable was captured and its message equals message.
hasThrowableWithMessageContaining(String substring)Assert a throwable was captured and its message contains substring.
hasNoThrowable()Assert no throwable was attached to this log entry.

LogCaptor API

logCaptor.getLogs() // unmodifiable snapshot at call time; call again for fresh viewlogCaptor.clearLogs() // remove all captured entries (called automatically in beforeEach)logCaptor.withMinLevel(Level) // lower effective capture level (restored in afterEach)logCaptor.resetConfiguration() // restore original log level (called automatically in afterEach)logCaptor.close() // resetConfiguration() + uninstall handler (called in afterAll)

LogEntry fields

FieldTypeDescription
timestampInstantWall-clock time of the event.
levelorg.slf4j.event.LevelSLF4J level (TRACE/DEBUG/INFO/WARN/ERROR).
loggerNameStringFully-qualified logger name.
formattedMessageStringFully resolved message — {} placeholders substituted.
rawTemplateStringRaw SLF4J template before substitution (may equal formattedMessage).
throwableThrowableInfoSerialized exception snapshot, or null.
mdcContextMap<String,String>Unmodifiable copy of MDC at log time.
threadNameStringName of the thread that emitted the event.
threadIdlongID of the thread that emitted the event.
markerNameStringSLF4J marker name — always null in v1.

Full example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.slf4j.MDC;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// unexpected ERRORs will fail the test@PrintLogsOnFailure// show captured logs when a test failsclassUserServiceTest {
privatestaticfinalLoggerlog = LoggerFactory.getLogger(UserServiceTest.class);
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidinfo_with_mdc() {
MDC.put("userId", "alice");
log.info("User {} logged in", "alice");
assertThat(logCaptor)
.atLevel(Level.INFO)
.withMdcEntry("userId", "alice")
.single()
.hasFormattedMessageContaining("alice")
.hasNoThrowable();
}
@Testvoiderror_with_exception() {
RuntimeExceptionex = newRuntimeException("boom");
log.error("Processing failed", ex);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.single()
.hasFormattedMessageContaining("Processing failed")
.hasThrowable(RuntimeException.class)
.hasThrowableWithMessage("boom");
}
@Test@EchoLogs(minimumLevel = Level.DEBUG) // print logs to console for this test onlyvoiddebug_trace_scenario() {
log.debug("step 1");
log.debug("step 2");
assertThat(logCaptor)
.atLevel(Level.DEBUG)
.hasSize(2);
}
}

Limitations

  • JBoss Log Manager only. The capture handler is a JBoss ExtHandler; it does not work with Logback or java.util.logging alone.
  • No concurrent test isolation. The capture handler is attached to the root logger and sees all log events in the JVM. Tests running concurrently share the same captor; log isolation is not guaranteed. A warning is printed when @Execution(CONCURRENT) is detected.
  • rawTemplate not available via JBoss bridge. When routing SLF4J through slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the resolved message. The raw template is not preserved by the bridge.
  • Markers not supported in v1.LogEntry.markerName() is always null.

About

In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager)

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - usmanakram232/log-assert: In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager) · GitHub
Skip to content

Repository files navigation

log-assert

Warning

Experimental. This library is under active development. APIs may change between minor versions without notice. Not recommended for production test suites yet — use at your own risk and pin to an exact version.

log-assert logo

CIMaven CentralDownloadsLicense

In-memory log capture with AssertJ-style assertions for JUnit 5.

Designed for Quarkus and any JBoss Log Manager environment.
SLF4J API + JUnit 5 extension + fluent AssertJ DSL.


Why log-assert?

Other approaches to log assertion in the JBoss/Quarkus ecosystem:

ApproachProblem
Logback ListAppenderVerbose setup per test; no DSL; does not work with JBoss Log Manager
Manual java.util.logging.HandlerBoilerplate; you write all the assertion logic yourself
Quarkus built-in log captureQuarkus-only; no standalone Java support; no fluent DSL

log-assert is the only library offering a fluent AssertJ-style DSL over JBoss Log Manager events with full Quarkus lifecycle awareness — works in plain JUnit 5 tests and @QuarkusTest alike.


Compatibility

DependencyMinimum version
Java21
JUnit 5 (junit-jupiter)5.10+
AssertJ3.24+
JBoss Log Manager3.0+
SLF4J2.0+
Quarkus (optional)3.x

Quick start

Maven dependency

<dependency>
<groupId>io.github.usmanakram232</groupId>
<artifactId>log-assert</artifactId>
<version>1.0.2</version>
<scope>test</scope>
</dependency>

Minimal example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
classOrderServiceTest {
@InjectLogCaptorLogCaptorlogCaptor; // auto-injected and auto-cleared before each test@Testvoidpayment_failure_is_logged() {
orderService.processPayment(invalidRequest);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.fromLogger(OrderService.class)
.hasSize(1)
.single()
.hasFormattedMessageContaining("Payment rejected")
.hasThrowable(IllegalArgumentException.class);
}
}

Quarkus setup

Quarkus re-initialises the JBoss Log Manager after the extension class is loaded, which would uninstall any handler registered via @ExtendWith. Use the @RegisterExtension static form so the extension runs after Quarkus has finished resetting the log manager:

importio.quarkus.test.junit.QuarkusTest;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.extension.RegisterExtension;
@QuarkusTestclassPaymentResourceTest {
@RegisterExtension// MUST be staticstaticLogCaptorExtensionlogExt = LogCaptorExtension.create();
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidcaptures_payment_error() {
// ... trigger code under test ...assertThat(logCaptor)
.atLevel(Level.ERROR)
.isNotEmpty();
}
}

Why static? JUnit 5's @RegisterExtension on a static field registers the extension as a class-level extension (like @BeforeAll/@AfterAll), giving it a chance to install the capture handler after the Quarkus test machinery has finished setting up the log manager. A non-static field would register it as an instance-level extension, which runs too early.


Annotations

@InjectLogCaptor

Marks a LogCaptor field on a test class for automatic injection by LogCaptorExtension. The field is set (and the captor is cleared) before each test method.

@InjectLogCaptorLogCaptorcaptor;

The captor can also be received as a method parameter:

@Testvoidexample(LogCaptorcaptor) { ... }

@PrintLogsOnFailure

When present on a test class or method, dumps all captured log entries to System.err when that test fails. Useful for diagnosing flaky or unexpected failures without permanently enabling verbose logging.

@ExtendWith(LogCaptorExtension.class)
@PrintLogsOnFailure// dump logs whenever any test in this class failsclassMyTest { ... }

Can also be placed on a single method:

@Test@PrintLogsOnFailure// dump logs only when this test failsvoidnoisy_integration_test() { ... }

@EchoLogs

Re-enables console log output for the annotated test scope. By default, LogCaptorExtension detaches all console/stream handlers from the root logger to prevent test noise. Annotating with @EchoLogs signals that log output should still be forwarded to the console for that scope.

@EchoLogs(minimumLevel = Level.DEBUG) // echo DEBUG and above to console during this test@Testvoiddebug_heavy_test() { ... }
AttributeDefaultDescription
minimumLevelTRACEMinimum SLF4J level to echo.

@FailOnUncheckedError

Causes the test to fail in afterEach if any ERROR-level log entries remain in the captor that were not asserted on. This is a safety net to catch unexpected error logging.

@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// any un-asserted ERROR entry fails the testclassStrictServiceTest { ... }

The check fires in afterEachonly when the test itself passed. If the test body already threw an exception or AssertionError, @FailOnUncheckedError is suppressed to avoid masking the original failure. If you asserted ERROR entries in the test body, call logCaptor.clearLogs() after your assertions to prevent a spurious @FailOnUncheckedError failure.


Assertion API reference

Entry point

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;

LogCaptorAssertions is deliberately named to avoid clashing with AssertJ's own org.assertj.core.api.Assertions when both are statically imported in the same class.

// From a LogCaptor (snapshot taken at call time)assertThat(logCaptor) ...
// From a pre-fetched listList<LogEntry> entries = logCaptor.getLogs();
assertThat(entries) ...

LogsAssert — filters (chainable, return new LogsAssert)

MethodDescription
atLevel(Level level)Keep only entries at exactly the given SLF4J level.
atLevelAtLeast(Level minimum)Keep only entries at minimum severity or higher.
fromLogger(Class<?> clazz)Keep only entries whose logger name starts with clazz.getName().
fromLogger(String prefix)Keep only entries whose logger name starts with prefix.
containingMessage(String substring)Keep only entries whose formatted message contains substring (case-sensitive).
withMdcEntry(String key, String value)Keep only entries whose MDC context has key mapped to value.

Filters return a newLogsAssert over the narrowed list and do not mutate the original.

LogsAssert — terminal assertions

MethodDescription
hasSize(int n)Assert exactly n entries. Failure message lists all captured entries.
isEmpty()Assert no entries captured.
isNotEmpty()Assert at least one entry captured.

LogsAssert — navigation (return LogEntryAssert)

MethodDescription
single()Assert exactly one entry, then return an assertion for it.
first()Assert at least one entry, then return an assertion for the first.
last()Assert at least one entry, then return an assertion for the last.

LogEntryAssert — single-entry assertions

Message

MethodDescription
hasFormattedMessage(String expected)Assert the fully-resolved message equals expected.
hasFormattedMessageContaining(String substring)Assert the resolved message contains substring.
hasFormattedMessageMatching(Pattern regex)Assert the resolved message matches the regex (uses find(), not matches()).
hasRawTemplate(String expected)Assert the raw SLF4J template before {} substitution.

Bridge note: when using slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the fully resolved string. Use hasFormattedMessageContaining in all environments.

Level & logger

MethodDescription
hasLevel(Level level)Assert the log level exactly.
hasLoggerName(String name)Assert the fully-qualified logger name exactly.

MDC

MethodDescription
hasMdcEntry(String key, String value)Assert the MDC context at log time contained key → value.

Throwable

MethodDescription
hasThrowable(Class<? extends Throwable> type)Assert a throwable was captured and its class equals type.
hasThrowableWithMessage(String message)Assert a throwable was captured and its message equals message.
hasThrowableWithMessageContaining(String substring)Assert a throwable was captured and its message contains substring.
hasNoThrowable()Assert no throwable was attached to this log entry.

LogCaptor API

logCaptor.getLogs() // unmodifiable snapshot at call time; call again for fresh viewlogCaptor.clearLogs() // remove all captured entries (called automatically in beforeEach)logCaptor.withMinLevel(Level) // lower effective capture level (restored in afterEach)logCaptor.resetConfiguration() // restore original log level (called automatically in afterEach)logCaptor.close() // resetConfiguration() + uninstall handler (called in afterAll)

LogEntry fields

FieldTypeDescription
timestampInstantWall-clock time of the event.
levelorg.slf4j.event.LevelSLF4J level (TRACE/DEBUG/INFO/WARN/ERROR).
loggerNameStringFully-qualified logger name.
formattedMessageStringFully resolved message — {} placeholders substituted.
rawTemplateStringRaw SLF4J template before substitution (may equal formattedMessage).
throwableThrowableInfoSerialized exception snapshot, or null.
mdcContextMap<String,String>Unmodifiable copy of MDC at log time.
threadNameStringName of the thread that emitted the event.
threadIdlongID of the thread that emitted the event.
markerNameStringSLF4J marker name — always null in v1.

Full example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.slf4j.MDC;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// unexpected ERRORs will fail the test@PrintLogsOnFailure// show captured logs when a test failsclassUserServiceTest {
privatestaticfinalLoggerlog = LoggerFactory.getLogger(UserServiceTest.class);
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidinfo_with_mdc() {
MDC.put("userId", "alice");
log.info("User {} logged in", "alice");
assertThat(logCaptor)
.atLevel(Level.INFO)
.withMdcEntry("userId", "alice")
.single()
.hasFormattedMessageContaining("alice")
.hasNoThrowable();
}
@Testvoiderror_with_exception() {
RuntimeExceptionex = newRuntimeException("boom");
log.error("Processing failed", ex);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.single()
.hasFormattedMessageContaining("Processing failed")
.hasThrowable(RuntimeException.class)
.hasThrowableWithMessage("boom");
}
@Test@EchoLogs(minimumLevel = Level.DEBUG) // print logs to console for this test onlyvoiddebug_trace_scenario() {
log.debug("step 1");
log.debug("step 2");
assertThat(logCaptor)
.atLevel(Level.DEBUG)
.hasSize(2);
}
}

Limitations

  • JBoss Log Manager only. The capture handler is a JBoss ExtHandler; it does not work with Logback or java.util.logging alone.
  • No concurrent test isolation. The capture handler is attached to the root logger and sees all log events in the JVM. Tests running concurrently share the same captor; log isolation is not guaranteed. A warning is printed when @Execution(CONCURRENT) is detected.
  • rawTemplate not available via JBoss bridge. When routing SLF4J through slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the resolved message. The raw template is not preserved by the bridge.
  • Markers not supported in v1.LogEntry.markerName() is always null.

About

In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager)

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - usmanakram232/log-assert: In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager) · GitHub
Skip to content

Repository files navigation

log-assert

Warning

Experimental. This library is under active development. APIs may change between minor versions without notice. Not recommended for production test suites yet — use at your own risk and pin to an exact version.

log-assert logo

CIMaven CentralDownloadsLicense

In-memory log capture with AssertJ-style assertions for JUnit 5.

Designed for Quarkus and any JBoss Log Manager environment.
SLF4J API + JUnit 5 extension + fluent AssertJ DSL.


Why log-assert?

Other approaches to log assertion in the JBoss/Quarkus ecosystem:

ApproachProblem
Logback ListAppenderVerbose setup per test; no DSL; does not work with JBoss Log Manager
Manual java.util.logging.HandlerBoilerplate; you write all the assertion logic yourself
Quarkus built-in log captureQuarkus-only; no standalone Java support; no fluent DSL

log-assert is the only library offering a fluent AssertJ-style DSL over JBoss Log Manager events with full Quarkus lifecycle awareness — works in plain JUnit 5 tests and @QuarkusTest alike.


Compatibility

DependencyMinimum version
Java21
JUnit 5 (junit-jupiter)5.10+
AssertJ3.24+
JBoss Log Manager3.0+
SLF4J2.0+
Quarkus (optional)3.x

Quick start

Maven dependency

<dependency>
<groupId>io.github.usmanakram232</groupId>
<artifactId>log-assert</artifactId>
<version>1.0.2</version>
<scope>test</scope>
</dependency>

Minimal example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
classOrderServiceTest {
@InjectLogCaptorLogCaptorlogCaptor; // auto-injected and auto-cleared before each test@Testvoidpayment_failure_is_logged() {
orderService.processPayment(invalidRequest);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.fromLogger(OrderService.class)
.hasSize(1)
.single()
.hasFormattedMessageContaining("Payment rejected")
.hasThrowable(IllegalArgumentException.class);
}
}

Quarkus setup

Quarkus re-initialises the JBoss Log Manager after the extension class is loaded, which would uninstall any handler registered via @ExtendWith. Use the @RegisterExtension static form so the extension runs after Quarkus has finished resetting the log manager:

importio.quarkus.test.junit.QuarkusTest;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.extension.RegisterExtension;
@QuarkusTestclassPaymentResourceTest {
@RegisterExtension// MUST be staticstaticLogCaptorExtensionlogExt = LogCaptorExtension.create();
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidcaptures_payment_error() {
// ... trigger code under test ...assertThat(logCaptor)
.atLevel(Level.ERROR)
.isNotEmpty();
}
}

Why static? JUnit 5's @RegisterExtension on a static field registers the extension as a class-level extension (like @BeforeAll/@AfterAll), giving it a chance to install the capture handler after the Quarkus test machinery has finished setting up the log manager. A non-static field would register it as an instance-level extension, which runs too early.


Annotations

@InjectLogCaptor

Marks a LogCaptor field on a test class for automatic injection by LogCaptorExtension. The field is set (and the captor is cleared) before each test method.

@InjectLogCaptorLogCaptorcaptor;

The captor can also be received as a method parameter:

@Testvoidexample(LogCaptorcaptor) { ... }

@PrintLogsOnFailure

When present on a test class or method, dumps all captured log entries to System.err when that test fails. Useful for diagnosing flaky or unexpected failures without permanently enabling verbose logging.

@ExtendWith(LogCaptorExtension.class)
@PrintLogsOnFailure// dump logs whenever any test in this class failsclassMyTest { ... }

Can also be placed on a single method:

@Test@PrintLogsOnFailure// dump logs only when this test failsvoidnoisy_integration_test() { ... }

@EchoLogs

Re-enables console log output for the annotated test scope. By default, LogCaptorExtension detaches all console/stream handlers from the root logger to prevent test noise. Annotating with @EchoLogs signals that log output should still be forwarded to the console for that scope.

@EchoLogs(minimumLevel = Level.DEBUG) // echo DEBUG and above to console during this test@Testvoiddebug_heavy_test() { ... }
AttributeDefaultDescription
minimumLevelTRACEMinimum SLF4J level to echo.

@FailOnUncheckedError

Causes the test to fail in afterEach if any ERROR-level log entries remain in the captor that were not asserted on. This is a safety net to catch unexpected error logging.

@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// any un-asserted ERROR entry fails the testclassStrictServiceTest { ... }

The check fires in afterEachonly when the test itself passed. If the test body already threw an exception or AssertionError, @FailOnUncheckedError is suppressed to avoid masking the original failure. If you asserted ERROR entries in the test body, call logCaptor.clearLogs() after your assertions to prevent a spurious @FailOnUncheckedError failure.


Assertion API reference

Entry point

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;

LogCaptorAssertions is deliberately named to avoid clashing with AssertJ's own org.assertj.core.api.Assertions when both are statically imported in the same class.

// From a LogCaptor (snapshot taken at call time)assertThat(logCaptor) ...
// From a pre-fetched listList<LogEntry> entries = logCaptor.getLogs();
assertThat(entries) ...

LogsAssert — filters (chainable, return new LogsAssert)

MethodDescription
atLevel(Level level)Keep only entries at exactly the given SLF4J level.
atLevelAtLeast(Level minimum)Keep only entries at minimum severity or higher.
fromLogger(Class<?> clazz)Keep only entries whose logger name starts with clazz.getName().
fromLogger(String prefix)Keep only entries whose logger name starts with prefix.
containingMessage(String substring)Keep only entries whose formatted message contains substring (case-sensitive).
withMdcEntry(String key, String value)Keep only entries whose MDC context has key mapped to value.

Filters return a newLogsAssert over the narrowed list and do not mutate the original.

LogsAssert — terminal assertions

MethodDescription
hasSize(int n)Assert exactly n entries. Failure message lists all captured entries.
isEmpty()Assert no entries captured.
isNotEmpty()Assert at least one entry captured.

LogsAssert — navigation (return LogEntryAssert)

MethodDescription
single()Assert exactly one entry, then return an assertion for it.
first()Assert at least one entry, then return an assertion for the first.
last()Assert at least one entry, then return an assertion for the last.

LogEntryAssert — single-entry assertions

Message

MethodDescription
hasFormattedMessage(String expected)Assert the fully-resolved message equals expected.
hasFormattedMessageContaining(String substring)Assert the resolved message contains substring.
hasFormattedMessageMatching(Pattern regex)Assert the resolved message matches the regex (uses find(), not matches()).
hasRawTemplate(String expected)Assert the raw SLF4J template before {} substitution.

Bridge note: when using slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the fully resolved string. Use hasFormattedMessageContaining in all environments.

Level & logger

MethodDescription
hasLevel(Level level)Assert the log level exactly.
hasLoggerName(String name)Assert the fully-qualified logger name exactly.

MDC

MethodDescription
hasMdcEntry(String key, String value)Assert the MDC context at log time contained key → value.

Throwable

MethodDescription
hasThrowable(Class<? extends Throwable> type)Assert a throwable was captured and its class equals type.
hasThrowableWithMessage(String message)Assert a throwable was captured and its message equals message.
hasThrowableWithMessageContaining(String substring)Assert a throwable was captured and its message contains substring.
hasNoThrowable()Assert no throwable was attached to this log entry.

LogCaptor API

logCaptor.getLogs() // unmodifiable snapshot at call time; call again for fresh viewlogCaptor.clearLogs() // remove all captured entries (called automatically in beforeEach)logCaptor.withMinLevel(Level) // lower effective capture level (restored in afterEach)logCaptor.resetConfiguration() // restore original log level (called automatically in afterEach)logCaptor.close() // resetConfiguration() + uninstall handler (called in afterAll)

LogEntry fields

FieldTypeDescription
timestampInstantWall-clock time of the event.
levelorg.slf4j.event.LevelSLF4J level (TRACE/DEBUG/INFO/WARN/ERROR).
loggerNameStringFully-qualified logger name.
formattedMessageStringFully resolved message — {} placeholders substituted.
rawTemplateStringRaw SLF4J template before substitution (may equal formattedMessage).
throwableThrowableInfoSerialized exception snapshot, or null.
mdcContextMap<String,String>Unmodifiable copy of MDC at log time.
threadNameStringName of the thread that emitted the event.
threadIdlongID of the thread that emitted the event.
markerNameStringSLF4J marker name — always null in v1.

Full example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.slf4j.MDC;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// unexpected ERRORs will fail the test@PrintLogsOnFailure// show captured logs when a test failsclassUserServiceTest {
privatestaticfinalLoggerlog = LoggerFactory.getLogger(UserServiceTest.class);
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidinfo_with_mdc() {
MDC.put("userId", "alice");
log.info("User {} logged in", "alice");
assertThat(logCaptor)
.atLevel(Level.INFO)
.withMdcEntry("userId", "alice")
.single()
.hasFormattedMessageContaining("alice")
.hasNoThrowable();
}
@Testvoiderror_with_exception() {
RuntimeExceptionex = newRuntimeException("boom");
log.error("Processing failed", ex);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.single()
.hasFormattedMessageContaining("Processing failed")
.hasThrowable(RuntimeException.class)
.hasThrowableWithMessage("boom");
}
@Test@EchoLogs(minimumLevel = Level.DEBUG) // print logs to console for this test onlyvoiddebug_trace_scenario() {
log.debug("step 1");
log.debug("step 2");
assertThat(logCaptor)
.atLevel(Level.DEBUG)
.hasSize(2);
}
}

Limitations

  • JBoss Log Manager only. The capture handler is a JBoss ExtHandler; it does not work with Logback or java.util.logging alone.
  • No concurrent test isolation. The capture handler is attached to the root logger and sees all log events in the JVM. Tests running concurrently share the same captor; log isolation is not guaranteed. A warning is printed when @Execution(CONCURRENT) is detected.
  • rawTemplate not available via JBoss bridge. When routing SLF4J through slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the resolved message. The raw template is not preserved by the bridge.
  • Markers not supported in v1.LogEntry.markerName() is always null.

About

In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager)

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - usmanakram232/log-assert: In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager) · GitHub
Skip to content

Repository files navigation

log-assert

Warning

Experimental. This library is under active development. APIs may change between minor versions without notice. Not recommended for production test suites yet — use at your own risk and pin to an exact version.

log-assert logo

CIMaven CentralDownloadsLicense

In-memory log capture with AssertJ-style assertions for JUnit 5.

Designed for Quarkus and any JBoss Log Manager environment.
SLF4J API + JUnit 5 extension + fluent AssertJ DSL.


Why log-assert?

Other approaches to log assertion in the JBoss/Quarkus ecosystem:

ApproachProblem
Logback ListAppenderVerbose setup per test; no DSL; does not work with JBoss Log Manager
Manual java.util.logging.HandlerBoilerplate; you write all the assertion logic yourself
Quarkus built-in log captureQuarkus-only; no standalone Java support; no fluent DSL

log-assert is the only library offering a fluent AssertJ-style DSL over JBoss Log Manager events with full Quarkus lifecycle awareness — works in plain JUnit 5 tests and @QuarkusTest alike.


Compatibility

DependencyMinimum version
Java21
JUnit 5 (junit-jupiter)5.10+
AssertJ3.24+
JBoss Log Manager3.0+
SLF4J2.0+
Quarkus (optional)3.x

Quick start

Maven dependency

<dependency>
<groupId>io.github.usmanakram232</groupId>
<artifactId>log-assert</artifactId>
<version>1.0.2</version>
<scope>test</scope>
</dependency>

Minimal example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
classOrderServiceTest {
@InjectLogCaptorLogCaptorlogCaptor; // auto-injected and auto-cleared before each test@Testvoidpayment_failure_is_logged() {
orderService.processPayment(invalidRequest);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.fromLogger(OrderService.class)
.hasSize(1)
.single()
.hasFormattedMessageContaining("Payment rejected")
.hasThrowable(IllegalArgumentException.class);
}
}

Quarkus setup

Quarkus re-initialises the JBoss Log Manager after the extension class is loaded, which would uninstall any handler registered via @ExtendWith. Use the @RegisterExtension static form so the extension runs after Quarkus has finished resetting the log manager:

importio.quarkus.test.junit.QuarkusTest;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.extension.RegisterExtension;
@QuarkusTestclassPaymentResourceTest {
@RegisterExtension// MUST be staticstaticLogCaptorExtensionlogExt = LogCaptorExtension.create();
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidcaptures_payment_error() {
// ... trigger code under test ...assertThat(logCaptor)
.atLevel(Level.ERROR)
.isNotEmpty();
}
}

Why static? JUnit 5's @RegisterExtension on a static field registers the extension as a class-level extension (like @BeforeAll/@AfterAll), giving it a chance to install the capture handler after the Quarkus test machinery has finished setting up the log manager. A non-static field would register it as an instance-level extension, which runs too early.


Annotations

@InjectLogCaptor

Marks a LogCaptor field on a test class for automatic injection by LogCaptorExtension. The field is set (and the captor is cleared) before each test method.

@InjectLogCaptorLogCaptorcaptor;

The captor can also be received as a method parameter:

@Testvoidexample(LogCaptorcaptor) { ... }

@PrintLogsOnFailure

When present on a test class or method, dumps all captured log entries to System.err when that test fails. Useful for diagnosing flaky or unexpected failures without permanently enabling verbose logging.

@ExtendWith(LogCaptorExtension.class)
@PrintLogsOnFailure// dump logs whenever any test in this class failsclassMyTest { ... }

Can also be placed on a single method:

@Test@PrintLogsOnFailure// dump logs only when this test failsvoidnoisy_integration_test() { ... }

@EchoLogs

Re-enables console log output for the annotated test scope. By default, LogCaptorExtension detaches all console/stream handlers from the root logger to prevent test noise. Annotating with @EchoLogs signals that log output should still be forwarded to the console for that scope.

@EchoLogs(minimumLevel = Level.DEBUG) // echo DEBUG and above to console during this test@Testvoiddebug_heavy_test() { ... }
AttributeDefaultDescription
minimumLevelTRACEMinimum SLF4J level to echo.

@FailOnUncheckedError

Causes the test to fail in afterEach if any ERROR-level log entries remain in the captor that were not asserted on. This is a safety net to catch unexpected error logging.

@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// any un-asserted ERROR entry fails the testclassStrictServiceTest { ... }

The check fires in afterEachonly when the test itself passed. If the test body already threw an exception or AssertionError, @FailOnUncheckedError is suppressed to avoid masking the original failure. If you asserted ERROR entries in the test body, call logCaptor.clearLogs() after your assertions to prevent a spurious @FailOnUncheckedError failure.


Assertion API reference

Entry point

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;

LogCaptorAssertions is deliberately named to avoid clashing with AssertJ's own org.assertj.core.api.Assertions when both are statically imported in the same class.

// From a LogCaptor (snapshot taken at call time)assertThat(logCaptor) ...
// From a pre-fetched listList<LogEntry> entries = logCaptor.getLogs();
assertThat(entries) ...

LogsAssert — filters (chainable, return new LogsAssert)

MethodDescription
atLevel(Level level)Keep only entries at exactly the given SLF4J level.
atLevelAtLeast(Level minimum)Keep only entries at minimum severity or higher.
fromLogger(Class<?> clazz)Keep only entries whose logger name starts with clazz.getName().
fromLogger(String prefix)Keep only entries whose logger name starts with prefix.
containingMessage(String substring)Keep only entries whose formatted message contains substring (case-sensitive).
withMdcEntry(String key, String value)Keep only entries whose MDC context has key mapped to value.

Filters return a newLogsAssert over the narrowed list and do not mutate the original.

LogsAssert — terminal assertions

MethodDescription
hasSize(int n)Assert exactly n entries. Failure message lists all captured entries.
isEmpty()Assert no entries captured.
isNotEmpty()Assert at least one entry captured.

LogsAssert — navigation (return LogEntryAssert)

MethodDescription
single()Assert exactly one entry, then return an assertion for it.
first()Assert at least one entry, then return an assertion for the first.
last()Assert at least one entry, then return an assertion for the last.

LogEntryAssert — single-entry assertions

Message

MethodDescription
hasFormattedMessage(String expected)Assert the fully-resolved message equals expected.
hasFormattedMessageContaining(String substring)Assert the resolved message contains substring.
hasFormattedMessageMatching(Pattern regex)Assert the resolved message matches the regex (uses find(), not matches()).
hasRawTemplate(String expected)Assert the raw SLF4J template before {} substitution.

Bridge note: when using slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the fully resolved string. Use hasFormattedMessageContaining in all environments.

Level & logger

MethodDescription
hasLevel(Level level)Assert the log level exactly.
hasLoggerName(String name)Assert the fully-qualified logger name exactly.

MDC

MethodDescription
hasMdcEntry(String key, String value)Assert the MDC context at log time contained key → value.

Throwable

MethodDescription
hasThrowable(Class<? extends Throwable> type)Assert a throwable was captured and its class equals type.
hasThrowableWithMessage(String message)Assert a throwable was captured and its message equals message.
hasThrowableWithMessageContaining(String substring)Assert a throwable was captured and its message contains substring.
hasNoThrowable()Assert no throwable was attached to this log entry.

LogCaptor API

logCaptor.getLogs() // unmodifiable snapshot at call time; call again for fresh viewlogCaptor.clearLogs() // remove all captured entries (called automatically in beforeEach)logCaptor.withMinLevel(Level) // lower effective capture level (restored in afterEach)logCaptor.resetConfiguration() // restore original log level (called automatically in afterEach)logCaptor.close() // resetConfiguration() + uninstall handler (called in afterAll)

LogEntry fields

FieldTypeDescription
timestampInstantWall-clock time of the event.
levelorg.slf4j.event.LevelSLF4J level (TRACE/DEBUG/INFO/WARN/ERROR).
loggerNameStringFully-qualified logger name.
formattedMessageStringFully resolved message — {} placeholders substituted.
rawTemplateStringRaw SLF4J template before substitution (may equal formattedMessage).
throwableThrowableInfoSerialized exception snapshot, or null.
mdcContextMap<String,String>Unmodifiable copy of MDC at log time.
threadNameStringName of the thread that emitted the event.
threadIdlongID of the thread that emitted the event.
markerNameStringSLF4J marker name — always null in v1.

Full example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.slf4j.MDC;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// unexpected ERRORs will fail the test@PrintLogsOnFailure// show captured logs when a test failsclassUserServiceTest {
privatestaticfinalLoggerlog = LoggerFactory.getLogger(UserServiceTest.class);
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidinfo_with_mdc() {
MDC.put("userId", "alice");
log.info("User {} logged in", "alice");
assertThat(logCaptor)
.atLevel(Level.INFO)
.withMdcEntry("userId", "alice")
.single()
.hasFormattedMessageContaining("alice")
.hasNoThrowable();
}
@Testvoiderror_with_exception() {
RuntimeExceptionex = newRuntimeException("boom");
log.error("Processing failed", ex);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.single()
.hasFormattedMessageContaining("Processing failed")
.hasThrowable(RuntimeException.class)
.hasThrowableWithMessage("boom");
}
@Test@EchoLogs(minimumLevel = Level.DEBUG) // print logs to console for this test onlyvoiddebug_trace_scenario() {
log.debug("step 1");
log.debug("step 2");
assertThat(logCaptor)
.atLevel(Level.DEBUG)
.hasSize(2);
}
}

Limitations

  • JBoss Log Manager only. The capture handler is a JBoss ExtHandler; it does not work with Logback or java.util.logging alone.
  • No concurrent test isolation. The capture handler is attached to the root logger and sees all log events in the JVM. Tests running concurrently share the same captor; log isolation is not guaranteed. A warning is printed when @Execution(CONCURRENT) is detected.
  • rawTemplate not available via JBoss bridge. When routing SLF4J through slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the resolved message. The raw template is not preserved by the bridge.
  • Markers not supported in v1.LogEntry.markerName() is always null.

About

In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager)

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - usmanakram232/log-assert: In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager) · GitHub
Skip to content

Repository files navigation

log-assert

Warning

Experimental. This library is under active development. APIs may change between minor versions without notice. Not recommended for production test suites yet — use at your own risk and pin to an exact version.

log-assert logo

CIMaven CentralDownloadsLicense

In-memory log capture with AssertJ-style assertions for JUnit 5.

Designed for Quarkus and any JBoss Log Manager environment.
SLF4J API + JUnit 5 extension + fluent AssertJ DSL.


Why log-assert?

Other approaches to log assertion in the JBoss/Quarkus ecosystem:

ApproachProblem
Logback ListAppenderVerbose setup per test; no DSL; does not work with JBoss Log Manager
Manual java.util.logging.HandlerBoilerplate; you write all the assertion logic yourself
Quarkus built-in log captureQuarkus-only; no standalone Java support; no fluent DSL

log-assert is the only library offering a fluent AssertJ-style DSL over JBoss Log Manager events with full Quarkus lifecycle awareness — works in plain JUnit 5 tests and @QuarkusTest alike.


Compatibility

DependencyMinimum version
Java21
JUnit 5 (junit-jupiter)5.10+
AssertJ3.24+
JBoss Log Manager3.0+
SLF4J2.0+
Quarkus (optional)3.x

Quick start

Maven dependency

<dependency>
<groupId>io.github.usmanakram232</groupId>
<artifactId>log-assert</artifactId>
<version>1.0.2</version>
<scope>test</scope>
</dependency>

Minimal example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
classOrderServiceTest {
@InjectLogCaptorLogCaptorlogCaptor; // auto-injected and auto-cleared before each test@Testvoidpayment_failure_is_logged() {
orderService.processPayment(invalidRequest);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.fromLogger(OrderService.class)
.hasSize(1)
.single()
.hasFormattedMessageContaining("Payment rejected")
.hasThrowable(IllegalArgumentException.class);
}
}

Quarkus setup

Quarkus re-initialises the JBoss Log Manager after the extension class is loaded, which would uninstall any handler registered via @ExtendWith. Use the @RegisterExtension static form so the extension runs after Quarkus has finished resetting the log manager:

importio.quarkus.test.junit.QuarkusTest;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.extension.RegisterExtension;
@QuarkusTestclassPaymentResourceTest {
@RegisterExtension// MUST be staticstaticLogCaptorExtensionlogExt = LogCaptorExtension.create();
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidcaptures_payment_error() {
// ... trigger code under test ...assertThat(logCaptor)
.atLevel(Level.ERROR)
.isNotEmpty();
}
}

Why static? JUnit 5's @RegisterExtension on a static field registers the extension as a class-level extension (like @BeforeAll/@AfterAll), giving it a chance to install the capture handler after the Quarkus test machinery has finished setting up the log manager. A non-static field would register it as an instance-level extension, which runs too early.


Annotations

@InjectLogCaptor

Marks a LogCaptor field on a test class for automatic injection by LogCaptorExtension. The field is set (and the captor is cleared) before each test method.

@InjectLogCaptorLogCaptorcaptor;

The captor can also be received as a method parameter:

@Testvoidexample(LogCaptorcaptor) { ... }

@PrintLogsOnFailure

When present on a test class or method, dumps all captured log entries to System.err when that test fails. Useful for diagnosing flaky or unexpected failures without permanently enabling verbose logging.

@ExtendWith(LogCaptorExtension.class)
@PrintLogsOnFailure// dump logs whenever any test in this class failsclassMyTest { ... }

Can also be placed on a single method:

@Test@PrintLogsOnFailure// dump logs only when this test failsvoidnoisy_integration_test() { ... }

@EchoLogs

Re-enables console log output for the annotated test scope. By default, LogCaptorExtension detaches all console/stream handlers from the root logger to prevent test noise. Annotating with @EchoLogs signals that log output should still be forwarded to the console for that scope.

@EchoLogs(minimumLevel = Level.DEBUG) // echo DEBUG and above to console during this test@Testvoiddebug_heavy_test() { ... }
AttributeDefaultDescription
minimumLevelTRACEMinimum SLF4J level to echo.

@FailOnUncheckedError

Causes the test to fail in afterEach if any ERROR-level log entries remain in the captor that were not asserted on. This is a safety net to catch unexpected error logging.

@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// any un-asserted ERROR entry fails the testclassStrictServiceTest { ... }

The check fires in afterEachonly when the test itself passed. If the test body already threw an exception or AssertionError, @FailOnUncheckedError is suppressed to avoid masking the original failure. If you asserted ERROR entries in the test body, call logCaptor.clearLogs() after your assertions to prevent a spurious @FailOnUncheckedError failure.


Assertion API reference

Entry point

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;

LogCaptorAssertions is deliberately named to avoid clashing with AssertJ's own org.assertj.core.api.Assertions when both are statically imported in the same class.

// From a LogCaptor (snapshot taken at call time)assertThat(logCaptor) ...
// From a pre-fetched listList<LogEntry> entries = logCaptor.getLogs();
assertThat(entries) ...

LogsAssert — filters (chainable, return new LogsAssert)

MethodDescription
atLevel(Level level)Keep only entries at exactly the given SLF4J level.
atLevelAtLeast(Level minimum)Keep only entries at minimum severity or higher.
fromLogger(Class<?> clazz)Keep only entries whose logger name starts with clazz.getName().
fromLogger(String prefix)Keep only entries whose logger name starts with prefix.
containingMessage(String substring)Keep only entries whose formatted message contains substring (case-sensitive).
withMdcEntry(String key, String value)Keep only entries whose MDC context has key mapped to value.

Filters return a newLogsAssert over the narrowed list and do not mutate the original.

LogsAssert — terminal assertions

MethodDescription
hasSize(int n)Assert exactly n entries. Failure message lists all captured entries.
isEmpty()Assert no entries captured.
isNotEmpty()Assert at least one entry captured.

LogsAssert — navigation (return LogEntryAssert)

MethodDescription
single()Assert exactly one entry, then return an assertion for it.
first()Assert at least one entry, then return an assertion for the first.
last()Assert at least one entry, then return an assertion for the last.

LogEntryAssert — single-entry assertions

Message

MethodDescription
hasFormattedMessage(String expected)Assert the fully-resolved message equals expected.
hasFormattedMessageContaining(String substring)Assert the resolved message contains substring.
hasFormattedMessageMatching(Pattern regex)Assert the resolved message matches the regex (uses find(), not matches()).
hasRawTemplate(String expected)Assert the raw SLF4J template before {} substitution.

Bridge note: when using slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the fully resolved string. Use hasFormattedMessageContaining in all environments.

Level & logger

MethodDescription
hasLevel(Level level)Assert the log level exactly.
hasLoggerName(String name)Assert the fully-qualified logger name exactly.

MDC

MethodDescription
hasMdcEntry(String key, String value)Assert the MDC context at log time contained key → value.

Throwable

MethodDescription
hasThrowable(Class<? extends Throwable> type)Assert a throwable was captured and its class equals type.
hasThrowableWithMessage(String message)Assert a throwable was captured and its message equals message.
hasThrowableWithMessageContaining(String substring)Assert a throwable was captured and its message contains substring.
hasNoThrowable()Assert no throwable was attached to this log entry.

LogCaptor API

logCaptor.getLogs() // unmodifiable snapshot at call time; call again for fresh viewlogCaptor.clearLogs() // remove all captured entries (called automatically in beforeEach)logCaptor.withMinLevel(Level) // lower effective capture level (restored in afterEach)logCaptor.resetConfiguration() // restore original log level (called automatically in afterEach)logCaptor.close() // resetConfiguration() + uninstall handler (called in afterAll)

LogEntry fields

FieldTypeDescription
timestampInstantWall-clock time of the event.
levelorg.slf4j.event.LevelSLF4J level (TRACE/DEBUG/INFO/WARN/ERROR).
loggerNameStringFully-qualified logger name.
formattedMessageStringFully resolved message — {} placeholders substituted.
rawTemplateStringRaw SLF4J template before substitution (may equal formattedMessage).
throwableThrowableInfoSerialized exception snapshot, or null.
mdcContextMap<String,String>Unmodifiable copy of MDC at log time.
threadNameStringName of the thread that emitted the event.
threadIdlongID of the thread that emitted the event.
markerNameStringSLF4J marker name — always null in v1.

Full example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.slf4j.MDC;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// unexpected ERRORs will fail the test@PrintLogsOnFailure// show captured logs when a test failsclassUserServiceTest {
privatestaticfinalLoggerlog = LoggerFactory.getLogger(UserServiceTest.class);
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidinfo_with_mdc() {
MDC.put("userId", "alice");
log.info("User {} logged in", "alice");
assertThat(logCaptor)
.atLevel(Level.INFO)
.withMdcEntry("userId", "alice")
.single()
.hasFormattedMessageContaining("alice")
.hasNoThrowable();
}
@Testvoiderror_with_exception() {
RuntimeExceptionex = newRuntimeException("boom");
log.error("Processing failed", ex);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.single()
.hasFormattedMessageContaining("Processing failed")
.hasThrowable(RuntimeException.class)
.hasThrowableWithMessage("boom");
}
@Test@EchoLogs(minimumLevel = Level.DEBUG) // print logs to console for this test onlyvoiddebug_trace_scenario() {
log.debug("step 1");
log.debug("step 2");
assertThat(logCaptor)
.atLevel(Level.DEBUG)
.hasSize(2);
}
}

Limitations

  • JBoss Log Manager only. The capture handler is a JBoss ExtHandler; it does not work with Logback or java.util.logging alone.
  • No concurrent test isolation. The capture handler is attached to the root logger and sees all log events in the JVM. Tests running concurrently share the same captor; log isolation is not guaranteed. A warning is printed when @Execution(CONCURRENT) is detected.
  • rawTemplate not available via JBoss bridge. When routing SLF4J through slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the resolved message. The raw template is not preserved by the bridge.
  • Markers not supported in v1.LogEntry.markerName() is always null.

About

In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager)

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - usmanakram232/log-assert: In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager) · GitHub
Skip to content

Repository files navigation

log-assert

Warning

Experimental. This library is under active development. APIs may change between minor versions without notice. Not recommended for production test suites yet — use at your own risk and pin to an exact version.

log-assert logo

CIMaven CentralDownloadsLicense

In-memory log capture with AssertJ-style assertions for JUnit 5.

Designed for Quarkus and any JBoss Log Manager environment.
SLF4J API + JUnit 5 extension + fluent AssertJ DSL.


Why log-assert?

Other approaches to log assertion in the JBoss/Quarkus ecosystem:

ApproachProblem
Logback ListAppenderVerbose setup per test; no DSL; does not work with JBoss Log Manager
Manual java.util.logging.HandlerBoilerplate; you write all the assertion logic yourself
Quarkus built-in log captureQuarkus-only; no standalone Java support; no fluent DSL

log-assert is the only library offering a fluent AssertJ-style DSL over JBoss Log Manager events with full Quarkus lifecycle awareness — works in plain JUnit 5 tests and @QuarkusTest alike.


Compatibility

DependencyMinimum version
Java21
JUnit 5 (junit-jupiter)5.10+
AssertJ3.24+
JBoss Log Manager3.0+
SLF4J2.0+
Quarkus (optional)3.x

Quick start

Maven dependency

<dependency>
<groupId>io.github.usmanakram232</groupId>
<artifactId>log-assert</artifactId>
<version>1.0.2</version>
<scope>test</scope>
</dependency>

Minimal example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
classOrderServiceTest {
@InjectLogCaptorLogCaptorlogCaptor; // auto-injected and auto-cleared before each test@Testvoidpayment_failure_is_logged() {
orderService.processPayment(invalidRequest);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.fromLogger(OrderService.class)
.hasSize(1)
.single()
.hasFormattedMessageContaining("Payment rejected")
.hasThrowable(IllegalArgumentException.class);
}
}

Quarkus setup

Quarkus re-initialises the JBoss Log Manager after the extension class is loaded, which would uninstall any handler registered via @ExtendWith. Use the @RegisterExtension static form so the extension runs after Quarkus has finished resetting the log manager:

importio.quarkus.test.junit.QuarkusTest;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.extension.RegisterExtension;
@QuarkusTestclassPaymentResourceTest {
@RegisterExtension// MUST be staticstaticLogCaptorExtensionlogExt = LogCaptorExtension.create();
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidcaptures_payment_error() {
// ... trigger code under test ...assertThat(logCaptor)
.atLevel(Level.ERROR)
.isNotEmpty();
}
}

Why static? JUnit 5's @RegisterExtension on a static field registers the extension as a class-level extension (like @BeforeAll/@AfterAll), giving it a chance to install the capture handler after the Quarkus test machinery has finished setting up the log manager. A non-static field would register it as an instance-level extension, which runs too early.


Annotations

@InjectLogCaptor

Marks a LogCaptor field on a test class for automatic injection by LogCaptorExtension. The field is set (and the captor is cleared) before each test method.

@InjectLogCaptorLogCaptorcaptor;

The captor can also be received as a method parameter:

@Testvoidexample(LogCaptorcaptor) { ... }

@PrintLogsOnFailure

When present on a test class or method, dumps all captured log entries to System.err when that test fails. Useful for diagnosing flaky or unexpected failures without permanently enabling verbose logging.

@ExtendWith(LogCaptorExtension.class)
@PrintLogsOnFailure// dump logs whenever any test in this class failsclassMyTest { ... }

Can also be placed on a single method:

@Test@PrintLogsOnFailure// dump logs only when this test failsvoidnoisy_integration_test() { ... }

@EchoLogs

Re-enables console log output for the annotated test scope. By default, LogCaptorExtension detaches all console/stream handlers from the root logger to prevent test noise. Annotating with @EchoLogs signals that log output should still be forwarded to the console for that scope.

@EchoLogs(minimumLevel = Level.DEBUG) // echo DEBUG and above to console during this test@Testvoiddebug_heavy_test() { ... }
AttributeDefaultDescription
minimumLevelTRACEMinimum SLF4J level to echo.

@FailOnUncheckedError

Causes the test to fail in afterEach if any ERROR-level log entries remain in the captor that were not asserted on. This is a safety net to catch unexpected error logging.

@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// any un-asserted ERROR entry fails the testclassStrictServiceTest { ... }

The check fires in afterEachonly when the test itself passed. If the test body already threw an exception or AssertionError, @FailOnUncheckedError is suppressed to avoid masking the original failure. If you asserted ERROR entries in the test body, call logCaptor.clearLogs() after your assertions to prevent a spurious @FailOnUncheckedError failure.


Assertion API reference

Entry point

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;

LogCaptorAssertions is deliberately named to avoid clashing with AssertJ's own org.assertj.core.api.Assertions when both are statically imported in the same class.

// From a LogCaptor (snapshot taken at call time)assertThat(logCaptor) ...
// From a pre-fetched listList<LogEntry> entries = logCaptor.getLogs();
assertThat(entries) ...

LogsAssert — filters (chainable, return new LogsAssert)

MethodDescription
atLevel(Level level)Keep only entries at exactly the given SLF4J level.
atLevelAtLeast(Level minimum)Keep only entries at minimum severity or higher.
fromLogger(Class<?> clazz)Keep only entries whose logger name starts with clazz.getName().
fromLogger(String prefix)Keep only entries whose logger name starts with prefix.
containingMessage(String substring)Keep only entries whose formatted message contains substring (case-sensitive).
withMdcEntry(String key, String value)Keep only entries whose MDC context has key mapped to value.

Filters return a newLogsAssert over the narrowed list and do not mutate the original.

LogsAssert — terminal assertions

MethodDescription
hasSize(int n)Assert exactly n entries. Failure message lists all captured entries.
isEmpty()Assert no entries captured.
isNotEmpty()Assert at least one entry captured.

LogsAssert — navigation (return LogEntryAssert)

MethodDescription
single()Assert exactly one entry, then return an assertion for it.
first()Assert at least one entry, then return an assertion for the first.
last()Assert at least one entry, then return an assertion for the last.

LogEntryAssert — single-entry assertions

Message

MethodDescription
hasFormattedMessage(String expected)Assert the fully-resolved message equals expected.
hasFormattedMessageContaining(String substring)Assert the resolved message contains substring.
hasFormattedMessageMatching(Pattern regex)Assert the resolved message matches the regex (uses find(), not matches()).
hasRawTemplate(String expected)Assert the raw SLF4J template before {} substitution.

Bridge note: when using slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the fully resolved string. Use hasFormattedMessageContaining in all environments.

Level & logger

MethodDescription
hasLevel(Level level)Assert the log level exactly.
hasLoggerName(String name)Assert the fully-qualified logger name exactly.

MDC

MethodDescription
hasMdcEntry(String key, String value)Assert the MDC context at log time contained key → value.

Throwable

MethodDescription
hasThrowable(Class<? extends Throwable> type)Assert a throwable was captured and its class equals type.
hasThrowableWithMessage(String message)Assert a throwable was captured and its message equals message.
hasThrowableWithMessageContaining(String substring)Assert a throwable was captured and its message contains substring.
hasNoThrowable()Assert no throwable was attached to this log entry.

LogCaptor API

logCaptor.getLogs() // unmodifiable snapshot at call time; call again for fresh viewlogCaptor.clearLogs() // remove all captured entries (called automatically in beforeEach)logCaptor.withMinLevel(Level) // lower effective capture level (restored in afterEach)logCaptor.resetConfiguration() // restore original log level (called automatically in afterEach)logCaptor.close() // resetConfiguration() + uninstall handler (called in afterAll)

LogEntry fields

FieldTypeDescription
timestampInstantWall-clock time of the event.
levelorg.slf4j.event.LevelSLF4J level (TRACE/DEBUG/INFO/WARN/ERROR).
loggerNameStringFully-qualified logger name.
formattedMessageStringFully resolved message — {} placeholders substituted.
rawTemplateStringRaw SLF4J template before substitution (may equal formattedMessage).
throwableThrowableInfoSerialized exception snapshot, or null.
mdcContextMap<String,String>Unmodifiable copy of MDC at log time.
threadNameStringName of the thread that emitted the event.
threadIdlongID of the thread that emitted the event.
markerNameStringSLF4J marker name — always null in v1.

Full example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.slf4j.MDC;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// unexpected ERRORs will fail the test@PrintLogsOnFailure// show captured logs when a test failsclassUserServiceTest {
privatestaticfinalLoggerlog = LoggerFactory.getLogger(UserServiceTest.class);
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidinfo_with_mdc() {
MDC.put("userId", "alice");
log.info("User {} logged in", "alice");
assertThat(logCaptor)
.atLevel(Level.INFO)
.withMdcEntry("userId", "alice")
.single()
.hasFormattedMessageContaining("alice")
.hasNoThrowable();
}
@Testvoiderror_with_exception() {
RuntimeExceptionex = newRuntimeException("boom");
log.error("Processing failed", ex);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.single()
.hasFormattedMessageContaining("Processing failed")
.hasThrowable(RuntimeException.class)
.hasThrowableWithMessage("boom");
}
@Test@EchoLogs(minimumLevel = Level.DEBUG) // print logs to console for this test onlyvoiddebug_trace_scenario() {
log.debug("step 1");
log.debug("step 2");
assertThat(logCaptor)
.atLevel(Level.DEBUG)
.hasSize(2);
}
}

Limitations

  • JBoss Log Manager only. The capture handler is a JBoss ExtHandler; it does not work with Logback or java.util.logging alone.
  • No concurrent test isolation. The capture handler is attached to the root logger and sees all log events in the JVM. Tests running concurrently share the same captor; log isolation is not guaranteed. A warning is printed when @Execution(CONCURRENT) is detected.
  • rawTemplate not available via JBoss bridge. When routing SLF4J through slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the resolved message. The raw template is not preserved by the bridge.
  • Markers not supported in v1.LogEntry.markerName() is always null.

About

In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager)

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - usmanakram232/log-assert: In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager) · GitHub
Skip to content

Repository files navigation

log-assert

Warning

Experimental. This library is under active development. APIs may change between minor versions without notice. Not recommended for production test suites yet — use at your own risk and pin to an exact version.

log-assert logo

CIMaven CentralDownloadsLicense

In-memory log capture with AssertJ-style assertions for JUnit 5.

Designed for Quarkus and any JBoss Log Manager environment.
SLF4J API + JUnit 5 extension + fluent AssertJ DSL.


Why log-assert?

Other approaches to log assertion in the JBoss/Quarkus ecosystem:

ApproachProblem
Logback ListAppenderVerbose setup per test; no DSL; does not work with JBoss Log Manager
Manual java.util.logging.HandlerBoilerplate; you write all the assertion logic yourself
Quarkus built-in log captureQuarkus-only; no standalone Java support; no fluent DSL

log-assert is the only library offering a fluent AssertJ-style DSL over JBoss Log Manager events with full Quarkus lifecycle awareness — works in plain JUnit 5 tests and @QuarkusTest alike.


Compatibility

DependencyMinimum version
Java21
JUnit 5 (junit-jupiter)5.10+
AssertJ3.24+
JBoss Log Manager3.0+
SLF4J2.0+
Quarkus (optional)3.x

Quick start

Maven dependency

<dependency>
<groupId>io.github.usmanakram232</groupId>
<artifactId>log-assert</artifactId>
<version>1.0.2</version>
<scope>test</scope>
</dependency>

Minimal example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
classOrderServiceTest {
@InjectLogCaptorLogCaptorlogCaptor; // auto-injected and auto-cleared before each test@Testvoidpayment_failure_is_logged() {
orderService.processPayment(invalidRequest);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.fromLogger(OrderService.class)
.hasSize(1)
.single()
.hasFormattedMessageContaining("Payment rejected")
.hasThrowable(IllegalArgumentException.class);
}
}

Quarkus setup

Quarkus re-initialises the JBoss Log Manager after the extension class is loaded, which would uninstall any handler registered via @ExtendWith. Use the @RegisterExtension static form so the extension runs after Quarkus has finished resetting the log manager:

importio.quarkus.test.junit.QuarkusTest;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.extension.RegisterExtension;
@QuarkusTestclassPaymentResourceTest {
@RegisterExtension// MUST be staticstaticLogCaptorExtensionlogExt = LogCaptorExtension.create();
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidcaptures_payment_error() {
// ... trigger code under test ...assertThat(logCaptor)
.atLevel(Level.ERROR)
.isNotEmpty();
}
}

Why static? JUnit 5's @RegisterExtension on a static field registers the extension as a class-level extension (like @BeforeAll/@AfterAll), giving it a chance to install the capture handler after the Quarkus test machinery has finished setting up the log manager. A non-static field would register it as an instance-level extension, which runs too early.


Annotations

@InjectLogCaptor

Marks a LogCaptor field on a test class for automatic injection by LogCaptorExtension. The field is set (and the captor is cleared) before each test method.

@InjectLogCaptorLogCaptorcaptor;

The captor can also be received as a method parameter:

@Testvoidexample(LogCaptorcaptor) { ... }

@PrintLogsOnFailure

When present on a test class or method, dumps all captured log entries to System.err when that test fails. Useful for diagnosing flaky or unexpected failures without permanently enabling verbose logging.

@ExtendWith(LogCaptorExtension.class)
@PrintLogsOnFailure// dump logs whenever any test in this class failsclassMyTest { ... }

Can also be placed on a single method:

@Test@PrintLogsOnFailure// dump logs only when this test failsvoidnoisy_integration_test() { ... }

@EchoLogs

Re-enables console log output for the annotated test scope. By default, LogCaptorExtension detaches all console/stream handlers from the root logger to prevent test noise. Annotating with @EchoLogs signals that log output should still be forwarded to the console for that scope.

@EchoLogs(minimumLevel = Level.DEBUG) // echo DEBUG and above to console during this test@Testvoiddebug_heavy_test() { ... }
AttributeDefaultDescription
minimumLevelTRACEMinimum SLF4J level to echo.

@FailOnUncheckedError

Causes the test to fail in afterEach if any ERROR-level log entries remain in the captor that were not asserted on. This is a safety net to catch unexpected error logging.

@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// any un-asserted ERROR entry fails the testclassStrictServiceTest { ... }

The check fires in afterEachonly when the test itself passed. If the test body already threw an exception or AssertionError, @FailOnUncheckedError is suppressed to avoid masking the original failure. If you asserted ERROR entries in the test body, call logCaptor.clearLogs() after your assertions to prevent a spurious @FailOnUncheckedError failure.


Assertion API reference

Entry point

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;

LogCaptorAssertions is deliberately named to avoid clashing with AssertJ's own org.assertj.core.api.Assertions when both are statically imported in the same class.

// From a LogCaptor (snapshot taken at call time)assertThat(logCaptor) ...
// From a pre-fetched listList<LogEntry> entries = logCaptor.getLogs();
assertThat(entries) ...

LogsAssert — filters (chainable, return new LogsAssert)

MethodDescription
atLevel(Level level)Keep only entries at exactly the given SLF4J level.
atLevelAtLeast(Level minimum)Keep only entries at minimum severity or higher.
fromLogger(Class<?> clazz)Keep only entries whose logger name starts with clazz.getName().
fromLogger(String prefix)Keep only entries whose logger name starts with prefix.
containingMessage(String substring)Keep only entries whose formatted message contains substring (case-sensitive).
withMdcEntry(String key, String value)Keep only entries whose MDC context has key mapped to value.

Filters return a newLogsAssert over the narrowed list and do not mutate the original.

LogsAssert — terminal assertions

MethodDescription
hasSize(int n)Assert exactly n entries. Failure message lists all captured entries.
isEmpty()Assert no entries captured.
isNotEmpty()Assert at least one entry captured.

LogsAssert — navigation (return LogEntryAssert)

MethodDescription
single()Assert exactly one entry, then return an assertion for it.
first()Assert at least one entry, then return an assertion for the first.
last()Assert at least one entry, then return an assertion for the last.

LogEntryAssert — single-entry assertions

Message

MethodDescription
hasFormattedMessage(String expected)Assert the fully-resolved message equals expected.
hasFormattedMessageContaining(String substring)Assert the resolved message contains substring.
hasFormattedMessageMatching(Pattern regex)Assert the resolved message matches the regex (uses find(), not matches()).
hasRawTemplate(String expected)Assert the raw SLF4J template before {} substitution.

Bridge note: when using slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the fully resolved string. Use hasFormattedMessageContaining in all environments.

Level & logger

MethodDescription
hasLevel(Level level)Assert the log level exactly.
hasLoggerName(String name)Assert the fully-qualified logger name exactly.

MDC

MethodDescription
hasMdcEntry(String key, String value)Assert the MDC context at log time contained key → value.

Throwable

MethodDescription
hasThrowable(Class<? extends Throwable> type)Assert a throwable was captured and its class equals type.
hasThrowableWithMessage(String message)Assert a throwable was captured and its message equals message.
hasThrowableWithMessageContaining(String substring)Assert a throwable was captured and its message contains substring.
hasNoThrowable()Assert no throwable was attached to this log entry.

LogCaptor API

logCaptor.getLogs() // unmodifiable snapshot at call time; call again for fresh viewlogCaptor.clearLogs() // remove all captured entries (called automatically in beforeEach)logCaptor.withMinLevel(Level) // lower effective capture level (restored in afterEach)logCaptor.resetConfiguration() // restore original log level (called automatically in afterEach)logCaptor.close() // resetConfiguration() + uninstall handler (called in afterAll)

LogEntry fields

FieldTypeDescription
timestampInstantWall-clock time of the event.
levelorg.slf4j.event.LevelSLF4J level (TRACE/DEBUG/INFO/WARN/ERROR).
loggerNameStringFully-qualified logger name.
formattedMessageStringFully resolved message — {} placeholders substituted.
rawTemplateStringRaw SLF4J template before substitution (may equal formattedMessage).
throwableThrowableInfoSerialized exception snapshot, or null.
mdcContextMap<String,String>Unmodifiable copy of MDC at log time.
threadNameStringName of the thread that emitted the event.
threadIdlongID of the thread that emitted the event.
markerNameStringSLF4J marker name — always null in v1.

Full example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.slf4j.MDC;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// unexpected ERRORs will fail the test@PrintLogsOnFailure// show captured logs when a test failsclassUserServiceTest {
privatestaticfinalLoggerlog = LoggerFactory.getLogger(UserServiceTest.class);
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidinfo_with_mdc() {
MDC.put("userId", "alice");
log.info("User {} logged in", "alice");
assertThat(logCaptor)
.atLevel(Level.INFO)
.withMdcEntry("userId", "alice")
.single()
.hasFormattedMessageContaining("alice")
.hasNoThrowable();
}
@Testvoiderror_with_exception() {
RuntimeExceptionex = newRuntimeException("boom");
log.error("Processing failed", ex);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.single()
.hasFormattedMessageContaining("Processing failed")
.hasThrowable(RuntimeException.class)
.hasThrowableWithMessage("boom");
}
@Test@EchoLogs(minimumLevel = Level.DEBUG) // print logs to console for this test onlyvoiddebug_trace_scenario() {
log.debug("step 1");
log.debug("step 2");
assertThat(logCaptor)
.atLevel(Level.DEBUG)
.hasSize(2);
}
}

Limitations

  • JBoss Log Manager only. The capture handler is a JBoss ExtHandler; it does not work with Logback or java.util.logging alone.
  • No concurrent test isolation. The capture handler is attached to the root logger and sees all log events in the JVM. Tests running concurrently share the same captor; log isolation is not guaranteed. A warning is printed when @Execution(CONCURRENT) is detected.
  • rawTemplate not available via JBoss bridge. When routing SLF4J through slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the resolved message. The raw template is not preserved by the bridge.
  • Markers not supported in v1.LogEntry.markerName() is always null.

About

In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager)

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - usmanakram232/log-assert: In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager) · GitHub
Skip to content

Repository files navigation

log-assert

Warning

Experimental. This library is under active development. APIs may change between minor versions without notice. Not recommended for production test suites yet — use at your own risk and pin to an exact version.

log-assert logo

CIMaven CentralDownloadsLicense

In-memory log capture with AssertJ-style assertions for JUnit 5.

Designed for Quarkus and any JBoss Log Manager environment.
SLF4J API + JUnit 5 extension + fluent AssertJ DSL.


Why log-assert?

Other approaches to log assertion in the JBoss/Quarkus ecosystem:

ApproachProblem
Logback ListAppenderVerbose setup per test; no DSL; does not work with JBoss Log Manager
Manual java.util.logging.HandlerBoilerplate; you write all the assertion logic yourself
Quarkus built-in log captureQuarkus-only; no standalone Java support; no fluent DSL

log-assert is the only library offering a fluent AssertJ-style DSL over JBoss Log Manager events with full Quarkus lifecycle awareness — works in plain JUnit 5 tests and @QuarkusTest alike.


Compatibility

DependencyMinimum version
Java21
JUnit 5 (junit-jupiter)5.10+
AssertJ3.24+
JBoss Log Manager3.0+
SLF4J2.0+
Quarkus (optional)3.x

Quick start

Maven dependency

<dependency>
<groupId>io.github.usmanakram232</groupId>
<artifactId>log-assert</artifactId>
<version>1.0.2</version>
<scope>test</scope>
</dependency>

Minimal example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
classOrderServiceTest {
@InjectLogCaptorLogCaptorlogCaptor; // auto-injected and auto-cleared before each test@Testvoidpayment_failure_is_logged() {
orderService.processPayment(invalidRequest);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.fromLogger(OrderService.class)
.hasSize(1)
.single()
.hasFormattedMessageContaining("Payment rejected")
.hasThrowable(IllegalArgumentException.class);
}
}

Quarkus setup

Quarkus re-initialises the JBoss Log Manager after the extension class is loaded, which would uninstall any handler registered via @ExtendWith. Use the @RegisterExtension static form so the extension runs after Quarkus has finished resetting the log manager:

importio.quarkus.test.junit.QuarkusTest;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.extension.RegisterExtension;
@QuarkusTestclassPaymentResourceTest {
@RegisterExtension// MUST be staticstaticLogCaptorExtensionlogExt = LogCaptorExtension.create();
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidcaptures_payment_error() {
// ... trigger code under test ...assertThat(logCaptor)
.atLevel(Level.ERROR)
.isNotEmpty();
}
}

Why static? JUnit 5's @RegisterExtension on a static field registers the extension as a class-level extension (like @BeforeAll/@AfterAll), giving it a chance to install the capture handler after the Quarkus test machinery has finished setting up the log manager. A non-static field would register it as an instance-level extension, which runs too early.


Annotations

@InjectLogCaptor

Marks a LogCaptor field on a test class for automatic injection by LogCaptorExtension. The field is set (and the captor is cleared) before each test method.

@InjectLogCaptorLogCaptorcaptor;

The captor can also be received as a method parameter:

@Testvoidexample(LogCaptorcaptor) { ... }

@PrintLogsOnFailure

When present on a test class or method, dumps all captured log entries to System.err when that test fails. Useful for diagnosing flaky or unexpected failures without permanently enabling verbose logging.

@ExtendWith(LogCaptorExtension.class)
@PrintLogsOnFailure// dump logs whenever any test in this class failsclassMyTest { ... }

Can also be placed on a single method:

@Test@PrintLogsOnFailure// dump logs only when this test failsvoidnoisy_integration_test() { ... }

@EchoLogs

Re-enables console log output for the annotated test scope. By default, LogCaptorExtension detaches all console/stream handlers from the root logger to prevent test noise. Annotating with @EchoLogs signals that log output should still be forwarded to the console for that scope.

@EchoLogs(minimumLevel = Level.DEBUG) // echo DEBUG and above to console during this test@Testvoiddebug_heavy_test() { ... }
AttributeDefaultDescription
minimumLevelTRACEMinimum SLF4J level to echo.

@FailOnUncheckedError

Causes the test to fail in afterEach if any ERROR-level log entries remain in the captor that were not asserted on. This is a safety net to catch unexpected error logging.

@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// any un-asserted ERROR entry fails the testclassStrictServiceTest { ... }

The check fires in afterEachonly when the test itself passed. If the test body already threw an exception or AssertionError, @FailOnUncheckedError is suppressed to avoid masking the original failure. If you asserted ERROR entries in the test body, call logCaptor.clearLogs() after your assertions to prevent a spurious @FailOnUncheckedError failure.


Assertion API reference

Entry point

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;

LogCaptorAssertions is deliberately named to avoid clashing with AssertJ's own org.assertj.core.api.Assertions when both are statically imported in the same class.

// From a LogCaptor (snapshot taken at call time)assertThat(logCaptor) ...
// From a pre-fetched listList<LogEntry> entries = logCaptor.getLogs();
assertThat(entries) ...

LogsAssert — filters (chainable, return new LogsAssert)

MethodDescription
atLevel(Level level)Keep only entries at exactly the given SLF4J level.
atLevelAtLeast(Level minimum)Keep only entries at minimum severity or higher.
fromLogger(Class<?> clazz)Keep only entries whose logger name starts with clazz.getName().
fromLogger(String prefix)Keep only entries whose logger name starts with prefix.
containingMessage(String substring)Keep only entries whose formatted message contains substring (case-sensitive).
withMdcEntry(String key, String value)Keep only entries whose MDC context has key mapped to value.

Filters return a newLogsAssert over the narrowed list and do not mutate the original.

LogsAssert — terminal assertions

MethodDescription
hasSize(int n)Assert exactly n entries. Failure message lists all captured entries.
isEmpty()Assert no entries captured.
isNotEmpty()Assert at least one entry captured.

LogsAssert — navigation (return LogEntryAssert)

MethodDescription
single()Assert exactly one entry, then return an assertion for it.
first()Assert at least one entry, then return an assertion for the first.
last()Assert at least one entry, then return an assertion for the last.

LogEntryAssert — single-entry assertions

Message

MethodDescription
hasFormattedMessage(String expected)Assert the fully-resolved message equals expected.
hasFormattedMessageContaining(String substring)Assert the resolved message contains substring.
hasFormattedMessageMatching(Pattern regex)Assert the resolved message matches the regex (uses find(), not matches()).
hasRawTemplate(String expected)Assert the raw SLF4J template before {} substitution.

Bridge note: when using slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the fully resolved string. Use hasFormattedMessageContaining in all environments.

Level & logger

MethodDescription
hasLevel(Level level)Assert the log level exactly.
hasLoggerName(String name)Assert the fully-qualified logger name exactly.

MDC

MethodDescription
hasMdcEntry(String key, String value)Assert the MDC context at log time contained key → value.

Throwable

MethodDescription
hasThrowable(Class<? extends Throwable> type)Assert a throwable was captured and its class equals type.
hasThrowableWithMessage(String message)Assert a throwable was captured and its message equals message.
hasThrowableWithMessageContaining(String substring)Assert a throwable was captured and its message contains substring.
hasNoThrowable()Assert no throwable was attached to this log entry.

LogCaptor API

logCaptor.getLogs() // unmodifiable snapshot at call time; call again for fresh viewlogCaptor.clearLogs() // remove all captured entries (called automatically in beforeEach)logCaptor.withMinLevel(Level) // lower effective capture level (restored in afterEach)logCaptor.resetConfiguration() // restore original log level (called automatically in afterEach)logCaptor.close() // resetConfiguration() + uninstall handler (called in afterAll)

LogEntry fields

FieldTypeDescription
timestampInstantWall-clock time of the event.
levelorg.slf4j.event.LevelSLF4J level (TRACE/DEBUG/INFO/WARN/ERROR).
loggerNameStringFully-qualified logger name.
formattedMessageStringFully resolved message — {} placeholders substituted.
rawTemplateStringRaw SLF4J template before substitution (may equal formattedMessage).
throwableThrowableInfoSerialized exception snapshot, or null.
mdcContextMap<String,String>Unmodifiable copy of MDC at log time.
threadNameStringName of the thread that emitted the event.
threadIdlongID of the thread that emitted the event.
markerNameStringSLF4J marker name — always null in v1.

Full example

importstaticio.github.logassert.assertj.LogCaptorAssertions.assertThat;
importio.github.logassert.core.LogCaptor;
importio.github.logassert.junit5.*;
importorg.junit.jupiter.api.*;
importorg.junit.jupiter.api.extension.*;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.slf4j.MDC;
importorg.slf4j.event.Level;
@ExtendWith(LogCaptorExtension.class)
@FailOnUncheckedError// unexpected ERRORs will fail the test@PrintLogsOnFailure// show captured logs when a test failsclassUserServiceTest {
privatestaticfinalLoggerlog = LoggerFactory.getLogger(UserServiceTest.class);
@InjectLogCaptorLogCaptorlogCaptor;
@Testvoidinfo_with_mdc() {
MDC.put("userId", "alice");
log.info("User {} logged in", "alice");
assertThat(logCaptor)
.atLevel(Level.INFO)
.withMdcEntry("userId", "alice")
.single()
.hasFormattedMessageContaining("alice")
.hasNoThrowable();
}
@Testvoiderror_with_exception() {
RuntimeExceptionex = newRuntimeException("boom");
log.error("Processing failed", ex);
assertThat(logCaptor)
.atLevel(Level.ERROR)
.single()
.hasFormattedMessageContaining("Processing failed")
.hasThrowable(RuntimeException.class)
.hasThrowableWithMessage("boom");
}
@Test@EchoLogs(minimumLevel = Level.DEBUG) // print logs to console for this test onlyvoiddebug_trace_scenario() {
log.debug("step 1");
log.debug("step 2");
assertThat(logCaptor)
.atLevel(Level.DEBUG)
.hasSize(2);
}
}

Limitations

  • JBoss Log Manager only. The capture handler is a JBoss ExtHandler; it does not work with Logback or java.util.logging alone.
  • No concurrent test isolation. The capture handler is attached to the root logger and sees all log events in the JVM. Tests running concurrently share the same captor; log isolation is not guaranteed. A warning is printed when @Execution(CONCURRENT) is detected.
  • rawTemplate not available via JBoss bridge. When routing SLF4J through slf4j-jboss-logmanager, both formattedMessage and rawTemplate contain the resolved message. The raw template is not preserved by the bridge.
  • Markers not supported in v1.LogEntry.markerName() is always null.

About

In-memory log capture with AssertJ-style assertions for JUnit 5 / Quarkus (JBoss Log Manager)

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages