Class LogCapture

java.lang.Object
io.github.flink.gcp.connector.testutils.LogCapture
All Implemented Interfaces:
AutoCloseable

@Internal public final class LogCapture extends Object implements AutoCloseable
Captures what a class logs, so a test can assert the line rather than only its side effects.

Deliberately narrow (#323). It is for the call sites where the log is the report: a handler whose whole behaviour is to log and drop, a failure absorbed during a shutdown the mailbox can no longer observe, a quota warning the job builds straight through, an abandoned teardown whose counter says one happened but not which client. Where a test already asserts a counter, a returned value or an absorbed exception that identifies the event, the log is a second report of something already covered — and asserting it there buys little while coupling the test to the message's wording, which is the cost that keeps this class from spreading.

Abbreviated, not compiled: the asserted message depends on the test scenario.


 try (LogCapture capture = LogCapture.of(FailureHandlers.LogAndDrop.class)) {
     FailureHandler.logAndDrop().handle(element);
     assertThat(capture.getMessages()).singleElement().satisfies(m -> ...);
 }
 

The backend is log4j2, which is what the test classpath actually carries — log4j-slf4j-impl reaches every module transitively through flink-test-utils. Logback is absent, so the ListAppender shape does not apply, and log4j's own ListAppender ships only in a log4j-core test-jar this build does not resolve. No log4j2 type appears in this class's signature, so replacing the backend is a change to this file alone.

Everything below exists to rule out one failure: a capture that collects nothing while looking exactly like a log that was never emitted.

  • The logger name is derived as slf4j derives it, Class#getName, because the code under test logs through slf4j. LogManager.getLogger(Class) does not agree: it prefers Class#getCanonicalName, so for a nested type such as FailureHandlers.LogAndDrop the two names differ by $ versus .. They are not even in an ancestor relationship, since log4j2 splits a name on . alone — so taking log4j2's name would attach to a logger nothing ever writes to.
  • The level is forced on the LoggerConfig, not on the Logger. Every module ships a log4j2-test.properties at rootLogger.level = WARN, so a WARN capture needs no help — but a capture below that does, and a module added without one falls back to log4j2's root ERROR, where even a WARN would be collected nowhere. Logger#setLevel would widen it, but it writes to the logger's cached config, which any later updateLoggers() — including one triggered by a second capture opening on an unrelated logger — silently discards.
  • The level is only ever widened, never narrowed, and is restored to the explicit level found, which may be none. Saving the effective level and writing it back would turn an inherited level into an explicit one and outlive the capture.

Restoration matters beyond tidiness: surefire reuses forks, so a LoggerConfig or a widened level left behind reaches every later test class in that fork (the shape of #316).

No level guard (if (LOG.isDebugEnabled())) exists in this repository's main sources, so widening cannot change what the code under test does. A guard added later would make that untrue for a capture taken below the ambient level.

A test that asserts a log was not emitted should sit beside one that asserts a log was, on the same logger: an empty capture is the expected result of both a working capture and a broken one, and only the positive case can tell them apart.

  • Nested Class Summary

    Nested Classes
    Modifier and Type
    Class
    Description
    static final class 
    One collected log event, flattened out of log4j2's own type.
    static enum 
    The levels a capture can be taken at, so no log4j2 type reaches this class's signature.
  • Method Summary

    Modifier and Type
    Method
    Description
    void
    Detaches the capture and restores what it changed.
    The events collected so far, in the order they were logged.
    The formatted messages collected so far, in the order they were logged.
    static LogCapture
    of(Class<?> owner)
    Captures WARN and above from the given class's logger, attaching immediately.
    static LogCapture
    of(Class<?> owner, LogCapture.Level level)
    Captures the given level and above from the given class's logger, attaching immediately.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Method Details

    • of

      public static LogCapture of(Class<?> owner)
      Captures WARN and above from the given class's logger, attaching immediately.
      Parameters:
      owner - the class whose logger to capture, as it was passed to getLogger
    • of

      public static LogCapture of(Class<?> owner, LogCapture.Level level)
      Captures the given level and above from the given class's logger, attaching immediately.
      Parameters:
      owner - the class whose logger to capture, as it was passed to getLogger
      level - the least specific level to collect
    • getMessages

      public List<String> getMessages()
      The formatted messages collected so far, in the order they were logged.
    • getEvents

      public List<LogCapture.Event> getEvents()
      The events collected so far, in the order they were logged.

      For the sites that log a cause — an absorbed shutdown report, an abandoned stream — where the attached throwable is part of what the line reports.

    • close

      public void close()
      Detaches the capture and restores what it changed. Idempotent.
      Specified by:
      close in interface AutoCloseable