Repository: kroxylicious/kroxylicious (java)

Issue:
VirtualCluster configuration logFrame/logNetwork not respected.

**Describe the bug**

I want to see the frames sent to/from my clients to a virtual cluster.  I ought to be able to 
do this with the `logFrame` option in the configuration file. 

Setting this `true` shows this is ineffective.

**To Reproduce**
Steps to reproduce the behavior:
1. Deploy to minikube with `./scripts/run-with-strimzi.sh kubernetes-examples/portperbroker_plain`
2. Run `kubectl edit cm -n kafka kroxylicious-config` and set `logFrames` to true
3. Run `kubectl rollout restart -n kafka deployment kroxylicious-proxy`
4. Run an example kafka command 
5. Run `kubectl logs -f deployment/kroxylicious-proxy -n kafka` 

**Actual behavior**
Empty log

**Expected behavior**
Logs such contain frames interaction.


**YAML files and logs**

Attach or copy paste your kroxylicious configuration YAML (with sensitive details
like broker address redacted).
Attach or copy and paste relevant logs.

**Additional context**
Add any other context about the problem here.


Pull request description:
Fix #472: Respect logFrame/logNetwork options


### Type of change

_Select the type of your PR_

- Bugfix

### Description

Respect logFrame/logNetwork configuration options.

* declare loggers in the log4j2 configuration file corresponding to the frame and network loggers
* also define two environment variables that make it easy to override useful loggers to aid problem resolution.

### Additional Context

_Why are you making this pull request?_

### Checklist

_Please go through this checklist and make sure all applicable tasks have been done_

- [ ] Write tests
- [ ] Make sure all tests pass
- [ ] Review performance test results. Ensure that any degradations to performance numbers are understood and justified.
- [ ] Make sure all Sonar-Lint warnings are addressed or are justifiably ignored.
- [ ] Update documentation
- [x] Reference relevant issue(s) and close them after merging
- [x] For user facing changes, update CHANGELOG.md (remember to include changes affecting the API of the test artefacts too).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a947dbe..b381120e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ Please enumerate **all user-facing** changes using format `<githib issue/pr numb
 
 ## 0.3.0
 
+* [#472](https://github.com/kroxylicious/kroxylicious/issues/472): Respect logFrame/logNetwork options in virtualcluster config
 * [#470](https://github.com/kroxylicious/kroxylicious/issues/470): Ensure that the EagerMetadataLearner passes on a client's metadata request with fidelity (fix for kcat -C -E)
 * [#416](https://github.com/kroxylicious/kroxylicious/issues/416): Eagerly expose broker endpoints on startup to allow existing client to reconnect (without connecting to bootstrap).
 * [#463](https://github.com/kroxylicious/kroxylicious/issues/463): deregister micrometer hooks, meters and the registry on shutdown
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/BannerLogger.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/BannerLogger.java
new file mode 100644
index 00000000..ae33353f
--- /dev/null
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/BannerLogger.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.proxy;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.event.Level;
+
+public class BannerLogger {
+
+    public static final String DEFAULT_BANNER_LOCATION = "banner.txt";
+    private final Logger targetLogger;
+    private final Supplier<Stream<String>> bannerReader;
+    private final Level targetLevel;
+
+    public BannerLogger() {
+        this(LoggerFactory.getLogger("io.kroxylicious.proxy.internal.StartupShutdownLogger"),
+                new BannerSupplier(DEFAULT_BANNER_LOCATION));
+    }
+
+    BannerLogger(Logger targetLogger, Supplier<Stream<String>> bannerReader) {
+        this.targetLogger = targetLogger;
+        this.bannerReader = bannerReader;
+        targetLevel = Level.INFO; // TODO should this be configurable?¡
+    }
+
+    public void log() {
+        bannerReader.get().forEach(line -> targetLogger.atLevel(targetLevel).log(line));
+    }
+
+    /* test */ static class BannerSupplier implements Supplier<Stream<String>> {
+
+        private final Supplier<Stream<String>> linesSupplier;
+        private final Set<String> licenseLines;
+
+        BannerSupplier(String resourceName) {
+            this(new FileLinesSupplier(resourceName), new FileLinesSupplier("etc/LICENSE.txt"));
+        }
+
+        BannerSupplier(Supplier<Stream<String>> bannerLinesSupplier, Supplier<Stream<String>> licenceStream) {
+            linesSupplier = bannerLinesSupplier;
+            licenseLines = Stream.concat(Stream.of("===="), licenceStream.get()).collect(Collectors.toSet());
+        }
+
+        @Override
+        public Stream<String> get() {
+            return linesSupplier.get().filter(Predicate.not(licenseLines::contains));
+        }
+    }
+
+    /* test */ static class FileLinesSupplier implements Supplier<Stream<String>> {
+
+        private final String resourceName;
+
+        FileLinesSupplier(String resourceName) {
+            this.resourceName = resourceName;
+        }
+
+        @Override
+        public Stream<String> get() {
+            final InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(resourceName);
+            if (Objects.isNull(resourceStream)) {
+                return Stream.empty();
+            }
+            else {
+                final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(resourceStream));
+                return bufferedReader.lines();
+            }
+        }
+    }
+}
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/KafkaProxy.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/KafkaProxy.java
index 92b27235..22b200cb 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/KafkaProxy.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/KafkaProxy.java
@@ -53,6 +53,7 @@ import io.kroxylicious.proxy.service.HostPort;
 public final class KafkaProxy implements AutoCloseable {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(KafkaProxy.class);
+    private static final Logger STARTUP_SHUTDOWN_LOGGER = LoggerFactory.getLogger("io.kroxylicious.proxy.internal.StartupShutdownLogger");
 
     private final NetworkBindingOperationProcessor bindingOperationProcessor = new DefaultNetworkBindingOperationProcessor();
     private final EndpointRegistry endpointRegistry = new EndpointRegistry(bindingOperationProcessor);
@@ -89,12 +90,12 @@ public final class KafkaProxy implements AutoCloseable {
      * @return This proxy.
      */
     public KafkaProxy startup() throws InterruptedException {
-        LOGGER.info("Kroxylicious starting");
-
         if (running.getAndSet(true)) {
             throw new IllegalStateException("This proxy is already running");
         }
 
+        logStartup();
+
         var portConflictDefector = new PortConflictDetector();
         Optional<HostPort> adminHttpHostPort = Optional.ofNullable(shouldBindAdminEndpoint() ? new HostPort(adminHttpConfig.host(), adminHttpConfig.port()) : null);
         portConflictDefector.validate(virtualClusterMap, adminHttpHostPort);
@@ -127,6 +128,10 @@ public final class KafkaProxy implements AutoCloseable {
         return this;
     }
 
+    private static void logStartup() {
+        new BannerLogger().log();
+    }
+
     private ServerBootstrap buildServerBootstrap(EventGroupConfig virtualHostEventGroup, KafkaProxyInitializer kafkaProxyInitializer) {
         return new ServerBootstrap().group(virtualHostEventGroup.bossGroup(), virtualHostEventGroup.workerGroup())
                 .channel(virtualHostEventGroup.clazz())
@@ -204,7 +209,7 @@ public final class KafkaProxy implements AutoCloseable {
             throw new IllegalStateException("This proxy is not running");
         }
         try {
-            LOGGER.info("Shutting down");
+            STARTUP_SHUTDOWN_LOGGER.info("Shutting down");
             endpointRegistry.shutdown().handle((u, t) -> {
                 bindingOperationProcessor.close();
                 var closeFutures = new ArrayList<Future<?>>();
diff --git a/kroxylicious/src/main/resources/banner.txt b/kroxylicious/src/main/resources/banner.txt
new file mode 100644
index 00000000..f086cb6a
--- /dev/null
+++ b/kroxylicious/src/main/resources/banner.txt
@@ -0,0 +1,93 @@
+====
+    Copyright Kroxylicious Authors.
+
+    Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+====
+
+@@@@@@@@@@@@@@@@@........@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@.-+**%%%#*=:.@@@...@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@.:*%%%##***#%%+.:=+++-.@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@.=%%*+========*%#%%%%%%*:@@@@@@@@@@@@@@@@@@@@@@..@@@
+@@@@@@@@@@@.-%#=============#*+==+*%#:@@@@@@@@@@@@....@@@.:**=.@
+@@@@@@@@@@@:%#===============*=====*%+.@@@@@@@@@@.+**+:@.-%%%%*.
+@@@@@@@@@@@=%================+======#%.@@@@@@@@@.+%##%#=+%#==*%-
+@@@@@@@@@@.+*=====+#=========+======+%-....:---++%*==*%%%#===+%+
+@@@@@@@@@@.*+=====#%*===============+%#%%%%%%%%%%%+==+++======%+
+@@@@@@@@@..#======#%+===============+###*****+++==============%+
+@@@@@@@@.-#%======+#=========================================+%=
+@@@@@@@.+%%*=================================================+%-
+@@@@@@.+%#===================================================*%:
+@@@@@.+%#====================================================#%.
+@@@@@:%#=====================================================#*.
+@@@@.+%+=====================================================%+.
+@@@@.%#=====================================================+%-@
+@@@@-%*=====================================================*%:@
+@@@@-%+=====================================================##.@
+@@@@+%+=====================================================%+.@
+@@@@+%+================================================+**#%%-@@
+@@@@-%+===========================================+**#%%%*-+%.@@
+@@@@-%+===========+=========================++**++%%#*++#+.%*.@@
+@@@@:%*==========*%+==================++**#%%=....*=====+%=%=@@@
+@@@@.##==========%#%#+=========++**##%%%%#**+*...-*===+*#%%#.@@@
+@@@@.*%====+=====*=+#%%######%%%%%##*++======++..=##%%%%%%%=.@@@
+@@@@@=%+===*+=========+*##****++===========+**%:.+%#*+-:.*#.@@@@
+@@@@@:%*===+#=========================+*##%%%%*#.*-...@@.=-.@@@@
+@@@@@.##====#+===================++*#%%%%*+=-..++#:@@@@@@..@@@@@
+@@@@@.=%+===*%===============+*#%%%%#+=-...@@@@:#%.@@@@@@.@@@@@@
+@@@@@@:%*====%#+=========+*#%%%#*+=%*.@@@@@@@@@.:#.@@@@@@@@@@@@@
+@@@@@@.#%====+%%#*****#%%%%%#*=====#%.@@@@@@@@@@.:.@@@@@@@@@@@@@
+@@@@@@.=%+====*%%%%%%%%%#*+========*%.@@@@@@@@@@@..@@@@@@@@@@@@@
+@@@@@@@:%*======*###**+============*%.@@@@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@.##=========================*%.@@@@@@@@@@@@@@@@@@@@@@@@@@
+@@@@....+%=========================##.@@@@@@@@@@@@@@@@@@@@@@@@@@
+@@@.=**++%+========================%*.@@@@@@@@@@@@@@@@@@@@@@@@@@
+@@.=%%%%%%+=======================+%=@@@@@@@@@@@@@@@@@@@@..@@@@@
+@@.+%==+*#========================*%:@@@@@@@@@@@@@@@@@@@..-@@@@@
+@@@-%#============================##.@@@@@@@@@@@@@@@@@@..#+.@@@@
+@@@.=%#===========================%+.@@@@@@@@@@@@@@@@@.-%%*.@@@@
+@@@@.=%%+========================+%-@@@@@@@@@@@@@@@@@.=%%%*.@@@@
+@@@@@.+%+========================*%:@@@@@@@@@@@@@@@.:*%#+#*.@@@@
+@@@@@:%#=========================#%.@@@@@@@@@@@@@..=%%*==%+.@@@@
+@@@@.+%+=========+===+===========#*.@@@@@@@@@@@..-#%#+===%+.@@@@
+@@@.-%*==========*==**===========%*...@.....:++=#%%*====+%-@@@@@
+@@@:#%===========#*##============%%===-=*+%%%%%%%*======#%.@@@@@
+@@.+%+===========+#*=============%%%%%%%#%%#*==+========%+.@@@@@
+@.-%*============================%*#++#+=+=====+=======*%-@@@@@@
+@:#%=============================##=========+#*=======+%*.@@@@@@
+.+%+=============================*%======+*##=========#%:@@@@@@@
+:%#===+==========================+%#**#%%%++=========#%=.@@@@@@@
+=%+===+#+=========++==============*#*%%**+==========#%+.@@@@@@@@
++%+====+%*========%==================+=============#%+.@@@@@@@@@
+:%%#****%%%+=====#*==============================+#%+.@@@@@@@@@@
+.:+#%%%%%*%%#+==*%==============================*%%=.@@@@@@@@@@@
+@@........:+%%#+%*============================*%%*:.@@@@@@@@@@@@
+@@@@@@@@@@@.:+%%%===========================*%%#=.@@@@@@@@@@@@@@
+@@@@@@@@@@@@@.:%*========================+#%%#=..@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@-%+=====+*+===========++*#%%%+-..@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@-%+===+#%%%%###**####%%%%#+-..@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@:%%##%%%+-=+**%%%%#**+=-:..@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@.-+#%*=:.@@...........@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@.....@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+
+888    d8P                                    888 d8b          d8b
+888   d8P                                     888 Y8P          Y8P
+888  d8P                                      888
+888d88K     888d888 .d88b.  888  888 888  888 888 888  .d8888b 888  .d88b.  888  888 .d8888b
+8888888b    888P"  d88""88b `Y8bd8P' 888  888 888 888 d88P"    888 d88""88b 888  888 88K
+888  Y88b   888    888  888   X88K   888  888 888 888 888      888 888  888 888  888 "Y8888b.
+888   Y88b  888    Y88..88P .d8""8b. Y88b 888 888 888 Y88b.    888 Y88..88P Y88b 888      X88
+888    Y88b 888     "Y88P"  888  888  "Y88888 888 888  "Y8888P 888  "Y88P"   "Y88888  88888P'
+                                          888
+                                     Y8b d88P
+                                      "Y88P"
+      d8b                        888                     888    d8b
+      Y8P                        888                     888    Y8P
+                                 888                     888
+      888 .d8888b       .d8888b  888888  8888b.  888d888 888888 888 88888b.   .d88b.
+      888 88K           88K      888        "88b 888P"   888    888 888 "88b d88P"88b
+      888 "Y8888b.      "Y8888b. 888    .d888888 888     888    888 888  888 888  888
+      888      X88           X88 Y88b.  888  888 888     Y88b.  888 888  888 Y88b 888
+      888  88888P'       88888P'  "Y888 "Y888888 888      "Y888 888 888  888  "Y88888
+                                                                                  888
+                                                                             Y8b d88P
+                                                                              "Y88P"
\ No newline at end of file
diff --git a/kroxylicious/src/main/resources/log4j2.properties b/kroxylicious/src/main/resources/log4j2.properties
index 570d4f10..12bf5f97 100644
--- a/kroxylicious/src/main/resources/log4j2.properties
+++ b/kroxylicious/src/main/resources/log4j2.properties
@@ -11,11 +11,29 @@ appender.console.name = STDOUT
 appender.console.layout.type = PatternLayout
 appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} <%t> %-5p %c:%L - %m%n
 
-rootLogger.level = WARN
+rootLogger.level = ${env:KROXYLICIOUS_ROOT_LOG_LEVEL:-WARN}
 rootLogger.appenderRefs = stdout
 rootLogger.appenderRef.console.ref = STDOUT
 rootLogger.additivity = false
 
-#logger.netty.name = io.netty
-#logger.netty.level = TRACE
-#logger.netty.additivity = false
+# The (Down|Up)streamNetworkLogger and (Down|Up)streamFrameLogger loggers are required to allow
+# the virtual cluster logNetwork/logFrames feature to work.  The log level needs to be in sync
+# with the logger level used in the code.
+logger.DownstreamNetworkLogger.name = io.kroxylicious.proxy.internal.DownstreamNetworkLogger
+logger.DownstreamNetworkLogger.level = INFO
+
+logger.UpstreamNetworkLogger.name = io.kroxylicious.proxy.internal.UpstreamNetworkLogger
+logger.UpstreamNetworkLogger.level = INFO
+
+logger.DownstreamFrameLogger.name = io.kroxylicious.proxy.internal.DownstreamFrameLogger
+logger.DownstreamFrameLogger.level = INFO
+
+logger.UpstreamFrameLogger.name = io.kroxylicious.proxy.internal.UpstreamFrameLogger
+logger.UpstreamFrameLogger.level = INFO
+
+logger.StartupShutdownLogger.name = io.kroxylicious.proxy.internal.StartupShutdownLogger
+logger.StartupShutdownLogger.level = INFO
+
+logger.test.name = io.kroxylicious.proxy.KafkaProxy
+logger.test.level = ${env:KROXYLICIOUS_LOG_LEVEL:-${env:KROXYLICIOUS_ROOT_LOG_LEVEL:-WARN}}
+


Test patch:
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/BannerLoggerTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/BannerLoggerTest.java
new file mode 100644
index 00000000..b86ae011
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/BannerLoggerTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.proxy;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.slf4j.Logger;
+import org.slf4j.spi.LoggingEventBuilder;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class BannerLoggerTest {
+
+    private BannerLogger bannerLogger;
+    @Mock
+    private LoggingEventBuilder loggingEventBuilder;
+
+    private Stream<String> bannerStream = Stream.empty();
+
+    @BeforeEach
+    void setUp() {
+        Logger testLogger = mock(Logger.class);
+        when(testLogger.atLevel(any())).thenReturn(loggingEventBuilder);
+        bannerLogger = new BannerLogger(testLogger, () -> bannerStream);
+    }
+
+    @Test
+    void shouldAppendToConfiguredLogger() {
+        // Given
+        bannerStream = Stream.of("banner_text");
+
+        // When
+        bannerLogger.log();
+
+        // Then
+        verify(loggingEventBuilder).log(anyString());
+    }
+
+    @Test
+    void shouldLogSingleLineBanner() {
+        // Given
+        bannerStream = Stream.of("banner_text");
+
+        // When
+        bannerLogger.log();
+
+        // Then
+        verify(loggingEventBuilder).log("banner_text");
+    }
+}
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/BannerSupplierTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/BannerSupplierTest.java
new file mode 100644
index 00000000..03c6e726
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/BannerSupplierTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.proxy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class BannerSupplierTest {
+
+    private List<String> licenseStream;
+
+    @BeforeEach
+    void setUp() {
+        licenseStream = List.of("====", "Copyright Kroxylicious Authors.",
+                "Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0");
+    }
+
+    @Test
+    void shouldRemoveLicenseHeader() {
+        // Given
+        final BannerLogger.BannerSupplier bannerSupplier = new BannerLogger.BannerSupplier(licenseStream::stream, licenseStream::stream);
+
+        // When
+        final Stream<String> actualBannerStream = bannerSupplier.get();
+
+        // Then
+        assertThat(actualBannerStream).isEmpty();
+    }
+
+    @Test
+    void shouldPassThroughBannerStream() {
+        // Given
+        final List<String> bannerLines = List.of("one", "two", "three");
+        final BannerLogger.BannerSupplier bannerSupplier = new BannerLogger.BannerSupplier(bannerLines::stream, Stream::empty);
+
+        // When
+        final Stream<String> actualBannerStream = bannerSupplier.get();
+
+        // Then
+        assertThat(actualBannerStream).containsExactlyInAnyOrderElementsOf(bannerLines);
+    }
+
+    @Test
+    void shouldPassThroughRemainingBannerStream() {
+        // Given
+        final List<String> expectedLines = List.of("one", "two", "three");
+        final List<String> bannerLines = new ArrayList<>();
+        bannerLines.addAll(licenseStream);
+        bannerLines.addAll(expectedLines);
+
+        final BannerLogger.BannerSupplier bannerSupplier = new BannerLogger.BannerSupplier(bannerLines::stream, licenseStream::stream);
+
+        // When
+        final Stream<String> actualBannerStream = bannerSupplier.get();
+
+        // Then
+        assertThat(actualBannerStream).containsExactlyInAnyOrderElementsOf(expectedLines);
+    }
+}
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/FileLinesSupplierTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/FileLinesSupplierTest.java
new file mode 100644
index 00000000..46d5bf30
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/FileLinesSupplierTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.proxy;
+
+import java.util.stream.Stream;
+
+import org.assertj.core.api.Condition;
+import org.junit.jupiter.api.Test;
+
+import static java.util.function.Predicate.not;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class FileLinesSupplierTest {
+
+    @Test
+    void shouldSupplyEmptyStreamForMissingResource() {
+        // Given
+        final BannerLogger.FileLinesSupplier bannerSupplier = new BannerLogger.FileLinesSupplier("random.txt");
+
+        // When
+        final Stream<String> actualStream = bannerSupplier.get();
+
+        // Then
+        assertThat(actualStream).isEmpty();
+    }
+
+    @Test
+    void shouldSupplyStreamForDefaultResource() {
+        // Given
+        final BannerLogger.FileLinesSupplier bannerSupplier = new BannerLogger.FileLinesSupplier("banner.txt");
+
+        // When
+        final Stream<String> actualStream = bannerSupplier.get();
+
+        // Then
+        assertThat(actualStream).areAtLeastOne(new Condition<>(not(String::isBlank), "Not blank"));
+    }
+}
\ No newline at end of file