Skip to content

Commit 3d877e6

Browse files
authored
Resolve no appender warning (#211)
* Resolve no appender warning * Improved disabling/enabling console output * Improved disabling/enabling console output * Moved getting console appender to AppenderUtils * Ensure Additive is set to false * Refactored a bit * Renamed appender * Removed redundant optional * Ensure LogCaptor is compatible with SpringBootTest * Cleanup * Improved test coverage * Improved test coverage * Made code more readable * Added test for reconfiguring root logger * Reduced constructor complexity * simplified enabling/disabling console output * Reordered method * Moved configuring of InMemoryAppender to AppenderUtils * Moved configuring of ConsoleAppender to AppenderUtils * Added author information * Applied sonar recommendations
1 parent 44dcebc commit 3d877e6

4 files changed

Lines changed: 344 additions & 35 deletions

File tree

src/main/java/nl/altindag/log/LogCaptor.java

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,27 @@
1818
import ch.qos.logback.classic.Level;
1919
import ch.qos.logback.classic.Logger;
2020
import ch.qos.logback.classic.spi.ILoggingEvent;
21-
import ch.qos.logback.core.Appender;
21+
import ch.qos.logback.core.ConsoleAppender;
2222
import ch.qos.logback.core.filter.Filter;
2323
import nl.altindag.log.appender.InMemoryAppender;
2424
import nl.altindag.log.model.LogEvent;
25+
import nl.altindag.log.util.AppenderUtils;
2526
import nl.altindag.log.util.JavaUtilLoggingLoggerUtils;
2627
import nl.altindag.log.util.LogbackUtils;
2728
import nl.altindag.log.util.Mappers;
2829

29-
import java.util.Arrays;
3030
import java.util.Collections;
3131
import java.util.HashMap;
3232
import java.util.List;
3333
import java.util.Map;
34-
import java.util.Objects;
3534
import java.util.Optional;
35+
import java.util.Spliterator;
36+
import java.util.Spliterators;
3637
import java.util.concurrent.CopyOnWriteArrayList;
3738
import java.util.function.Function;
3839
import java.util.function.Predicate;
3940
import java.util.stream.Collectors;
41+
import java.util.stream.StreamSupport;
4042

4143
import static org.slf4j.Logger.ROOT_LOGGER_NAME;
4244

@@ -46,18 +48,16 @@
4648
public final class LogCaptor implements AutoCloseable {
4749

4850
private static final Map<String, Level> logLevelContainer = new HashMap<>();
49-
private static final Map<String, Appender<ILoggingEvent>> consoleAppenderContainer = new HashMap<>();
50-
private static final List<String> CONSOLE_APPENDER_NAMES = Arrays.asList("console", "CONSOLE");
5151

5252
private final Logger logger;
53-
private final Appender<ILoggingEvent> appender;
53+
private final InMemoryAppender<ILoggingEvent> inMemoryAppender;
54+
private ConsoleAppender<ILoggingEvent> consoleAppender;
5455
private final List<ILoggingEvent> eventsCollector = new CopyOnWriteArrayList<>();
5556

5657
private LogCaptor(String loggerName) {
5758
logger = LogbackUtils.getLogger(loggerName);
58-
appender = new InMemoryAppender<>("log-captor", eventsCollector);
59-
appender.start();
60-
logger.addAppender(appender);
59+
inMemoryAppender = AppenderUtils.configureInMemoryAppender(logger, eventsCollector);
60+
consoleAppender = AppenderUtils.configureConsoleAppender(logger);
6161

6262
JavaUtilLoggingLoggerUtils.redirectToSlf4j(loggerName);
6363
logLevelContainer.putIfAbsent(logger.getName(), logger.getEffectiveLevel());
@@ -163,7 +163,7 @@ private boolean hasMessage(Level level, String message) {
163163
}
164164

165165
public void addFilter(Filter<ILoggingEvent> filter) {
166-
appender.addFilter(filter);
166+
inMemoryAppender.addFilter(filter);
167167
filter.start();
168168
}
169169

@@ -214,30 +214,24 @@ public void disableLogs() {
214214
* LogCaptor will still be capturing the log entries.
215215
*/
216216
public void disableConsoleOutput() {
217-
getConsoleAppender().ifPresent(consoleAppender -> {
218-
getRootLogger().detachAppender(consoleAppender);
219-
consoleAppenderContainer.put(logger.getName(), consoleAppender);
220-
});
217+
reconfigure();
218+
logger.detachAppender(consoleAppender);
221219
}
222220

223221
/**
224222
* The output of the log entries to the console are enabled by default but can be re-enabled if
225223
* they are disabled earlier by {@link LogCaptor#disableConsoleOutput()}
226224
*/
227225
public void enableConsoleOutput() {
228-
Optional.ofNullable(consoleAppenderContainer.remove(logger.getName())).ifPresent(getRootLogger()::addAppender);
226+
reconfigure();
229227
}
230228

231-
Optional<Appender<ILoggingEvent>> getConsoleAppender() {
232-
Logger rootLogger = getRootLogger();
233-
return CONSOLE_APPENDER_NAMES.stream()
234-
.map(rootLogger::getAppender)
235-
.filter(Objects::nonNull)
236-
.findFirst();
229+
Logger getRootLogger() {
230+
return logger.getLoggerContext().getLogger(ROOT_LOGGER_NAME);
237231
}
238232

239-
private Logger getRootLogger() {
240-
return logger.getLoggerContext().getLogger(ROOT_LOGGER_NAME);
233+
Logger getLogger() {
234+
return logger;
241235
}
242236

243237
/**
@@ -253,10 +247,25 @@ public void clearLogs() {
253247
eventsCollector.clear();
254248
}
255249

250+
public void reconfigure() {
251+
StreamSupport.stream(Spliterators.spliteratorUnknownSize(logger.iteratorForAppenders(), Spliterator.ORDERED), false)
252+
.forEach(logger::detachAppender);
253+
254+
consoleAppender = AppenderUtils.configureConsoleAppender(logger, consoleAppender);
255+
logger.addAppender(inMemoryAppender);
256+
logger.addAppender(consoleAppender);
257+
inMemoryAppender.start();
258+
consoleAppender.start();
259+
}
260+
256261
@Override
257262
public void close() {
258-
logger.detachAppender(appender);
259-
appender.stop();
263+
logger.detachAppender(inMemoryAppender);
264+
inMemoryAppender.stop();
265+
if (!ROOT_LOGGER_NAME.equals(logger.getName())) {
266+
logger.setAdditive(true);
267+
logger.detachAppender(consoleAppender);
268+
}
260269
}
261270

262271
@Override
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright 2019 Thunderberry.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package nl.altindag.log.util;
17+
18+
import ch.qos.logback.classic.Logger;
19+
import ch.qos.logback.classic.LoggerContext;
20+
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
21+
import ch.qos.logback.classic.spi.ILoggingEvent;
22+
import ch.qos.logback.core.Appender;
23+
import ch.qos.logback.core.ConsoleAppender;
24+
import ch.qos.logback.core.encoder.Encoder;
25+
import nl.altindag.log.appender.InMemoryAppender;
26+
27+
import java.util.Arrays;
28+
import java.util.Iterator;
29+
import java.util.List;
30+
import java.util.Objects;
31+
import java.util.Optional;
32+
33+
import static org.slf4j.Logger.ROOT_LOGGER_NAME;
34+
35+
/**
36+
* @author Hakan Altindag
37+
*/
38+
public final class AppenderUtils {
39+
40+
public static final String CONSOLE_APPENDER_NAME = "console";
41+
public static final String IN_MEMORY_APPENDER_NAME = "logcaptor-in-memory-appender";
42+
private static final String DEFAULT_LOG_PATTERN = "%d{yyyy-MM-dd HH:mm:ss.SSSXXX} %-5level [%thread] %logger{36} - %msg%n";
43+
private static final List<String> CONSOLE_APPENDER_NAMES = Arrays.asList("console", "CONSOLE");
44+
45+
private AppenderUtils() {}
46+
47+
public static ConsoleAppender<ILoggingEvent> createConsoleAppender(LoggerContext loggerContext) {
48+
Encoder<ILoggingEvent> encoder = createEncoder(loggerContext);
49+
ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>();
50+
consoleAppender.setContext(loggerContext);
51+
consoleAppender.setName(CONSOLE_APPENDER_NAME);
52+
consoleAppender.setImmediateFlush(true);
53+
consoleAppender.setEncoder(encoder);
54+
consoleAppender.start();
55+
return consoleAppender;
56+
}
57+
58+
public static Encoder<ILoggingEvent> createEncoder(LoggerContext loggerContext) {
59+
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
60+
encoder.setContext(loggerContext);
61+
encoder.setPattern(DEFAULT_LOG_PATTERN);
62+
encoder.start();
63+
return encoder;
64+
}
65+
66+
public static Optional<ConsoleAppender<ILoggingEvent>> getConsoleAppender(Logger logger) {
67+
return CONSOLE_APPENDER_NAMES.stream()
68+
.map(logger::getAppender)
69+
.filter(Objects::nonNull)
70+
.filter(ConsoleAppender.class::isInstance)
71+
.map(consoleAppender -> (ConsoleAppender<ILoggingEvent>) consoleAppender)
72+
.findFirst();
73+
}
74+
75+
public static InMemoryAppender<ILoggingEvent> configureInMemoryAppender(Logger logger, List<ILoggingEvent> eventsCollector) {
76+
InMemoryAppender<ILoggingEvent> inMemoryAppender = new InMemoryAppender<>(AppenderUtils.IN_MEMORY_APPENDER_NAME, eventsCollector);
77+
inMemoryAppender.setContext(logger.getLoggerContext());
78+
inMemoryAppender.start();
79+
logger.addAppender(inMemoryAppender);
80+
return inMemoryAppender;
81+
}
82+
83+
public static ConsoleAppender<ILoggingEvent> configureConsoleAppender(Logger logger) {
84+
return configureConsoleAppender(logger, null);
85+
}
86+
87+
public static ConsoleAppender<ILoggingEvent> configureConsoleAppender(Logger logger, ConsoleAppender<ILoggingEvent> appender) {
88+
String loggerName = logger.getName();
89+
if (!ROOT_LOGGER_NAME.equals(loggerName)) {
90+
logger.setAdditive(false);
91+
}
92+
93+
ConsoleAppender<ILoggingEvent> consoleAppender = createConsoleAppender(logger, appender);
94+
if (!ROOT_LOGGER_NAME.equals(loggerName)) {
95+
boolean containsRootConsoleAppender = false;
96+
Iterator<Appender<ILoggingEvent>> rootAppenders = getRootLogger(logger).iteratorForAppenders();
97+
while (rootAppenders.hasNext()) {
98+
if (rootAppenders.next() instanceof ConsoleAppender) {
99+
containsRootConsoleAppender = true;
100+
}
101+
}
102+
103+
if (containsRootConsoleAppender) {
104+
logger.addAppender(consoleAppender);
105+
}
106+
}
107+
return consoleAppender;
108+
}
109+
110+
private static ConsoleAppender<ILoggingEvent> createConsoleAppender(Logger logger, ConsoleAppender<ILoggingEvent> consoleAppender) {
111+
return Optional.ofNullable(consoleAppender)
112+
.orElseGet(() -> AppenderUtils.getConsoleAppender(getRootLogger(logger))
113+
.orElseGet(() -> AppenderUtils.getConsoleAppender(logger)
114+
.orElseGet(() -> AppenderUtils.createConsoleAppender(logger.getLoggerContext()))));
115+
}
116+
117+
private static Logger getRootLogger(Logger logger) {
118+
return logger.getLoggerContext().getLogger(ROOT_LOGGER_NAME);
119+
}
120+
121+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2019 Thunderberry.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package nl.altindag.log;
17+
18+
import nl.altindag.console.ConsoleCaptor;
19+
import org.junit.jupiter.api.AfterAll;
20+
import org.junit.jupiter.api.BeforeAll;
21+
import org.junit.jupiter.api.Test;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
class ConsoleOutputShould {
28+
29+
private static final Logger LOGGER = LoggerFactory.getLogger(ConsoleOutputShould.class);
30+
31+
private static final ConsoleCaptor consoleCaptor = new ConsoleCaptor();
32+
private static final LogCaptor rootLogCaptor = LogCaptor.forRoot();
33+
private static final LogCaptor fooLogCaptor = LogCaptor.forClass(Foo.class);
34+
private static final LogCaptor barLogCaptor = LogCaptor.forClass(Bar.class);
35+
36+
@BeforeAll
37+
static void setUp() {
38+
fooLogCaptor.disableConsoleOutput();
39+
}
40+
41+
@AfterAll
42+
static void tearDown() {
43+
consoleCaptor.close();
44+
rootLogCaptor.close();
45+
fooLogCaptor.close();
46+
barLogCaptor.close();
47+
}
48+
49+
@Test
50+
void properlyLogToConsoleEvenWhenTogglingDisableFunction() {
51+
Foo foo = new Foo();
52+
Bar bar = new Bar();
53+
54+
{
55+
LOGGER.info("Calling foo");
56+
foo.foo();
57+
58+
assertThat(rootLogCaptor.getLogs()).contains("Calling foo");
59+
assertThat(fooLogCaptor.getLogs()).contains("Inside foo");
60+
assertThat(consoleCaptor.getStandardOutput().stream().anyMatch(line -> line.contains("Calling foo"))).isTrue();
61+
assertThat(consoleCaptor.getStandardOutput().stream().anyMatch(line -> line.contains("Inside foo"))).isFalse();
62+
63+
consoleCaptor.clearOutput();
64+
rootLogCaptor.clearLogs();
65+
fooLogCaptor.clearLogs();
66+
}
67+
68+
{
69+
LOGGER.info("Calling bar");
70+
bar.bar();
71+
72+
assertThat(rootLogCaptor.getLogs()).contains("Calling bar");
73+
assertThat(barLogCaptor.getLogs()).contains("Inside bar");
74+
assertThat(consoleCaptor.getStandardOutput().stream().anyMatch(line -> line.contains("Calling bar"))).isTrue();
75+
assertThat(consoleCaptor.getStandardOutput().stream().anyMatch(line -> line.contains("Inside bar"))).isTrue();
76+
77+
consoleCaptor.clearOutput();
78+
rootLogCaptor.clearLogs();
79+
barLogCaptor.clearLogs();
80+
}
81+
82+
{
83+
rootLogCaptor.disableConsoleOutput();
84+
fooLogCaptor.enableConsoleOutput();
85+
86+
LOGGER.info("Calling foo");
87+
foo.foo();
88+
89+
assertThat(rootLogCaptor.getLogs()).contains("Calling foo");
90+
assertThat(fooLogCaptor.getLogs()).contains("Inside foo");
91+
assertThat(consoleCaptor.getStandardOutput().stream().anyMatch(line -> line.contains("Calling foo"))).isFalse();
92+
assertThat(consoleCaptor.getStandardOutput().stream().anyMatch(line -> line.contains("Inside foo"))).isTrue();
93+
rootLogCaptor.enableConsoleOutput();
94+
}
95+
}
96+
97+
static class Foo {
98+
99+
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(Foo.class);
100+
101+
void foo() {
102+
LOGGER.info("Inside foo");
103+
}
104+
105+
}
106+
107+
static class Bar {
108+
109+
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(Bar.class);
110+
111+
void bar() {
112+
LOGGER.info("Inside bar");
113+
}
114+
115+
}
116+
117+
}

0 commit comments

Comments
 (0)