Skip to content

Commit 68e96af

Browse files
committed
Improved disabling/enabling console output
1 parent a68d395 commit 68e96af

5 files changed

Lines changed: 255 additions & 55 deletions

File tree

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

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,19 @@
1919
import ch.qos.logback.classic.Logger;
2020
import ch.qos.logback.classic.spi.ILoggingEvent;
2121
import ch.qos.logback.core.Appender;
22+
import ch.qos.logback.core.ConsoleAppender;
2223
import ch.qos.logback.core.filter.Filter;
2324
import nl.altindag.log.appender.InMemoryAppender;
24-
import nl.altindag.log.appender.NOPAppender;
2525
import nl.altindag.log.model.LogEvent;
26+
import nl.altindag.log.util.AppenderUtils;
2627
import nl.altindag.log.util.JavaUtilLoggingLoggerUtils;
2728
import nl.altindag.log.util.LogbackUtils;
2829
import nl.altindag.log.util.Mappers;
2930

3031
import java.util.Arrays;
3132
import java.util.Collections;
3233
import java.util.HashMap;
34+
import java.util.Iterator;
3335
import java.util.List;
3436
import java.util.Map;
3537
import java.util.Objects;
@@ -50,17 +52,50 @@ public final class LogCaptor implements AutoCloseable {
5052
private static final List<String> CONSOLE_APPENDER_NAMES = Arrays.asList("console", "CONSOLE");
5153

5254
private final Logger logger;
53-
private final Appender<ILoggingEvent> appender;
55+
private final InMemoryAppender<ILoggingEvent> inMemoryAppender;
56+
private final ConsoleAppender<ILoggingEvent> consoleAppender;
5457
private final List<ILoggingEvent> eventsCollector = new CopyOnWriteArrayList<>();
5558

5659
private LogCaptor(String loggerName) {
5760
logger = LogbackUtils.getLogger(loggerName);
58-
appender = new InMemoryAppender<>("log-captor", eventsCollector);
59-
appender.start();
60-
logger.addAppender(appender);
61+
inMemoryAppender = new InMemoryAppender<>("log-captor", eventsCollector);
62+
inMemoryAppender.start();
63+
logger.addAppender(inMemoryAppender);
6164

6265
JavaUtilLoggingLoggerUtils.redirectToSlf4j(loggerName);
6366
logLevelContainer.putIfAbsent(logger.getName(), logger.getEffectiveLevel());
67+
consoleAppender = configureConsoleAppender(loggerName);
68+
}
69+
70+
private ConsoleAppender<ILoggingEvent> configureConsoleAppender(String loggerName) {
71+
Optional<ConsoleAppender<ILoggingEvent>> consoleAppender;
72+
if (ROOT_LOGGER_NAME.equals(loggerName)) {
73+
Logger rootLogger = getRootLogger();
74+
consoleAppender = getConsoleAppender(rootLogger);
75+
} else {
76+
logger.setAdditive(false); // prevent log messages to be propagated to the root logger
77+
consoleAppender = getConsoleAppender(logger);
78+
}
79+
80+
if (!consoleAppender.isPresent()) {
81+
consoleAppender = Optional.of(AppenderUtils.createConsoleAppender(logger.getLoggerContext()));
82+
}
83+
84+
if (!ROOT_LOGGER_NAME.equals(loggerName)) {
85+
boolean containsRootConsoleAppender = false;
86+
Iterator<Appender<ILoggingEvent>> rootAppenders = getRootLogger().iteratorForAppenders();
87+
while (rootAppenders.hasNext()) {
88+
if (rootAppenders.next() instanceof ConsoleAppender) {
89+
containsRootConsoleAppender = true;
90+
}
91+
}
92+
93+
if (containsRootConsoleAppender) {
94+
logger.addAppender(consoleAppender.get());
95+
}
96+
}
97+
98+
return consoleAppender.get();
6499
}
65100

66101
/**
@@ -163,7 +198,7 @@ private boolean hasMessage(Level level, String message) {
163198
}
164199

165200
public void addFilter(Filter<ILoggingEvent> filter) {
166-
appender.addFilter(filter);
201+
inMemoryAppender.addFilter(filter);
167202
filter.start();
168203
}
169204

@@ -214,23 +249,30 @@ public void disableLogs() {
214249
* LogCaptor will still be capturing the log entries.
215250
*/
216251
public void disableConsoleOutput() {
217-
logger.setAdditive(false);
218-
logger.addAppender(new NOPAppender<>(logger.getLoggerContext()));
252+
Optional.ofNullable(consoleAppender).ifPresent(logger::detachAppender);
253+
if (!logger.iteratorForAppenders().hasNext()) {
254+
logger.addAppender(AppenderUtils.createNopAppender(logger.getLoggerContext()));
255+
}
219256
}
220257

221258
/**
222259
* The output of the log entries to the console are enabled by default but can be re-enabled if
223260
* they are disabled earlier by {@link LogCaptor#disableConsoleOutput()}
224261
*/
225262
public void enableConsoleOutput() {
226-
logger.setAdditive(true);
227-
logger.detachAppender(NOPAppender.APPENDER_NAME);
263+
logger.detachAppender(AppenderUtils.NOP_APPENDER_NAME);
264+
logger.addAppender(consoleAppender);
265+
if (!consoleAppender.isStarted()) {
266+
consoleAppender.start();
267+
}
228268
}
229269

230-
Optional<Appender<ILoggingEvent>> getConsoleAppender(Logger logger) {
270+
Optional<ConsoleAppender<ILoggingEvent>> getConsoleAppender(Logger logger) {
231271
return CONSOLE_APPENDER_NAMES.stream()
232272
.map(logger::getAppender)
233273
.filter(Objects::nonNull)
274+
.filter(ConsoleAppender.class::isInstance)
275+
.map(consoleAppender -> (ConsoleAppender<ILoggingEvent>) consoleAppender)
234276
.findFirst();
235277
}
236278

@@ -257,8 +299,12 @@ public void clearLogs() {
257299

258300
@Override
259301
public void close() {
260-
logger.detachAppender(appender);
261-
appender.stop();
302+
logger.detachAppender(inMemoryAppender);
303+
inMemoryAppender.stop();
304+
if (!ROOT_LOGGER_NAME.equals(logger.getName())) {
305+
logger.setAdditive(true);
306+
logger.detachAppender(consoleAppender);
307+
}
262308
}
263309

264310
@Override

src/main/java/nl/altindag/log/appender/NOPAppender.java

Lines changed: 0 additions & 35 deletions
This file was deleted.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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.LoggerContext;
19+
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
20+
import ch.qos.logback.classic.spi.ILoggingEvent;
21+
import ch.qos.logback.core.ConsoleAppender;
22+
import ch.qos.logback.core.Context;
23+
import ch.qos.logback.core.helpers.NOPAppender;
24+
25+
public final class AppenderUtils {
26+
27+
public static final String NOP_APPENDER_NAME = "logcaptor-nop-appender";
28+
public static final String CONSOLE_APPENDER_NAME = "console";
29+
private static final String DEFAULT_LOG_PATTERN = "%d{yyyy-MM-dd HH:mm:ss.SSSXXX} %-5level [%thread] %logger{36} - %msg%n";
30+
31+
private AppenderUtils() {}
32+
33+
public static ConsoleAppender<ILoggingEvent> createConsoleAppender(LoggerContext loggerContext) {
34+
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
35+
encoder.setContext(loggerContext);
36+
encoder.setPattern(DEFAULT_LOG_PATTERN);
37+
encoder.start();
38+
39+
ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>();
40+
consoleAppender.setContext(loggerContext);
41+
consoleAppender.setName(CONSOLE_APPENDER_NAME);
42+
consoleAppender.setImmediateFlush(true);
43+
consoleAppender.setEncoder(encoder);
44+
consoleAppender.start();
45+
return consoleAppender;
46+
}
47+
48+
public static NOPAppender<ILoggingEvent> createNopAppender(Context context) {
49+
NOPAppender<ILoggingEvent> nopAppender = new NOPAppender<>();
50+
nopAppender.setName(NOP_APPENDER_NAME);
51+
nopAppender.setContext(context);
52+
nopAppender.start();
53+
return nopAppender;
54+
}
55+
56+
}
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 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 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)