Add StringAppender Logback Appender implementation to capture log messages in-memory appended to a String.

Resolve gh-73.
This commit is contained in:
John Blum
2020-02-12 08:13:28 -08:00
parent bbe8ff65e0
commit 5cabe1662d
2 changed files with 485 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package org.springframework.geode.logging.slf4j.logback;
import java.util.Optional;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.Context;
/**
* {@link StringAppender} is a {@link Appender} implementation that captures all log events/statements in-memory
* appended to a {@link String} using optionally either a builder or a buffer.
*
* @author John Blum
* @see java.lang.StringBuilder
* @see java.lang.StringBuffer
* @see ch.qos.logback.core.Appender
* @see ch.qos.logback.core.AppenderBase
* @see ch.qos.logback.core.Context
* @since 1.3.0
*/
@SuppressWarnings("unused")
public class StringAppender extends AppenderBase<ILoggingEvent> {
protected static final String DEFAULT_NAME = "string";
@FunctionalInterface
interface StringAppenderWrapper {
void append(CharSequence charSequence);
}
protected static final StringAppenderWrapper stringBuilderAppenderWrapper = new StringAppenderWrapper() {
private final StringBuilder stringBuilder = new StringBuilder();
@Override
public void append(CharSequence charSequence) {
this.stringBuilder.append(charSequence);
}
@Override
public java.lang.String toString() {
return this.stringBuilder.toString();
}
};
protected static final StringAppenderWrapper stringBufferAppenderWrapper = new StringAppenderWrapper() {
private final StringBuffer stringBuffer = new StringBuffer();
@Override
public void append(CharSequence charSequence) {
this.stringBuffer.append(charSequence);
}
@Override
public java.lang.String toString() {
return this.stringBuffer.toString();
}
};
@SuppressWarnings({ "rawtypes", "unchecked", "unused" })
public static class Builder {
private static final boolean DEFAULT_REPLACE = false;
private boolean replace = false;
private boolean useSynchronization = false;
private Context context;
private DelegatingAppender delegate;
private ch.qos.logback.classic.Logger logger;
private String name;
public Builder applyTo(DelegatingAppender<?> delegate) {
return applyTo(delegate, DEFAULT_REPLACE);
}
public Builder applyTo(DelegatingAppender<?> delegate, boolean replace) {
this.delegate = delegate;
this.replace = replace;
return this;
}
public Builder applyTo(ch.qos.logback.classic.Logger logger) {
this.logger = logger;
return this;
}
public Builder setContext(Context context) {
this.context = context;
return this;
}
public Builder setName(String name) {
this.name = name;
return this;
}
public Builder useSynchronization() {
this.useSynchronization = true;
return this;
}
private Optional<DelegatingAppender> getDelegate() {
return Optional.ofNullable(this.delegate);
}
private Optional<ch.qos.logback.classic.Logger> getLogger() {
return Optional.ofNullable(this.logger);
}
private Context resolveContext() {
return this.context != null
? this.context
: Optional.ofNullable(LoggerFactory.getILoggerFactory())
.filter(Context.class::isInstance)
.map(Context.class::cast)
.orElse(null);
}
private String resolveName() {
return this.name != null && !this.name.trim().isEmpty() ? this.name : DEFAULT_NAME;
}
private StringAppenderWrapper resolveStringAppenderWrapper() {
return this.useSynchronization ? stringBufferAppenderWrapper : stringBuilderAppenderWrapper;
}
public StringAppender build() {
StringAppender stringAppender =
new StringAppender(resolveStringAppenderWrapper());
stringAppender.setContext(resolveContext());
stringAppender.setName(resolveName());
getDelegate().ifPresent(delegate -> {
Appender appender = this.replace ? stringAppender
: CompositeAppender.compose(delegate.getAppender(), stringAppender);
delegate.setAppender(appender);
});
getLogger().ifPresent(logger -> logger.addAppender(stringAppender));
return stringAppender;
}
public StringAppender buildAndStart() {
StringAppender stringAppender = build();
stringAppender.start();
return stringAppender;
}
}
private final StringAppenderWrapper stringAppenderWrapper;
protected StringAppender(StringAppenderWrapper stringAppenderWrapper) {
if (stringAppenderWrapper == null) {
throw new IllegalArgumentException("StringAppenderWrapper must not be null");
}
this.stringAppenderWrapper = stringAppenderWrapper;
}
public String getLogOutput() {
return getStringAppenderWrapper().toString();
}
protected StringAppenderWrapper getStringAppenderWrapper() {
return this.stringAppenderWrapper;
}
@Override
protected void append(ILoggingEvent loggingEvent) {
Optional.ofNullable(loggingEvent)
.map(event -> preProcessLogMessage(toString(event)))
.filter(this::isValidLogMessage)
.ifPresent(getStringAppenderWrapper()::append);
}
protected boolean isValidLogMessage(String message) {
return message != null && !message.isEmpty();
}
protected String preProcessLogMessage(String message) {
return message != null ? message.trim() : null;
}
protected String toString(ILoggingEvent loggingEvent) {
return loggingEvent != null ? loggingEvent.getFormattedMessage() : null;
}
}

View File

@@ -0,0 +1,264 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package org.springframework.geode.logging.slf4j.logback;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.Context;
/**
* Unit Tests for {@link StringAppender}.
*
* @author John Blum
* @see org.junit.Test
* @see org.mockito.Mock
* @see org.mockito.Mockito
* @see org.mockito.junit.MockitoJUnitRunner
* @see org.springframework.geode.logging.slf4j.logback.StringAppender
* @see ch.qos.logback.classic.Logger
* @see ch.qos.logback.core.Appender
* @since 1.3.0
*/
@RunWith(MockitoJUnitRunner.class)
@SuppressWarnings({ "rawtypes", "unchecked" })
public class StringAppenderUnitTests {
@Mock
private StringAppender.StringAppenderWrapper mockWrapper;
@After
public void tearDown() {
((Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)).detachAndStopAllAppenders();
}
@Test
public void buildStringAppender() {
StringAppender stringAppender = new StringAppender.Builder().build();
assertThat(stringAppender).isNotNull();
assertThat(stringAppender.isStarted()).isFalse();
assertThat(stringAppender.getContext()).isEqualTo(LoggerFactory.getILoggerFactory());
assertThat(stringAppender.getName()).isEqualTo(StringAppender.DEFAULT_NAME);
assertThat(stringAppender.getStringAppenderWrapper()).isSameAs(StringAppender.stringBuilderAppenderWrapper);
}
@Test
public void buildStringAppenderApplyToDelegateUsingReplace() {
DelegatingAppender delegate = spy(new DelegatingAppender());
StringAppender stringAppender = new StringAppender.Builder()
.applyTo(delegate, true)
.build();
assertThat(stringAppender).isNotNull();
assertThat(stringAppender.isStarted()).isFalse();
assertThat(stringAppender.getName()).isEqualTo(StringAppender.DEFAULT_NAME);
verify(delegate, times(1)).setAppender(eq(stringAppender));
assertThat(delegate.getAppender()).isEqualTo(stringAppender);
}
@Test
public void buildAndStartStringAppender() {
Context mockContext = mock(Context.class);
DelegatingAppender delegate = spy(new DelegatingAppender());
Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
StringAppender stringAppender = new StringAppender.Builder()
.applyTo(delegate)
.applyTo(rootLogger)
.setContext(mockContext)
.setName("TestStringAppender")
.useSynchronization()
.buildAndStart();
assertThat(stringAppender).isNotNull();
assertThat(stringAppender.isStarted()).isTrue();
assertThat(stringAppender.getContext()).isEqualTo(mockContext);
assertThat(stringAppender.getName()).isEqualTo("TestStringAppender");
assertThat(stringAppender.getStringAppenderWrapper()).isEqualTo(StringAppender.stringBufferAppenderWrapper);
assertThat(rootLogger.getAppender("TestStringAppender")).isEqualTo(stringAppender);
verify(delegate, times(1)).setAppender(isA(CompositeAppender.class));
Appender compositeAppender = delegate.getAppender();
assertThat(compositeAppender).isInstanceOf(CompositeAppender.class);
assertThat(((CompositeAppender) compositeAppender).getAppenderOne()).isEqualTo(DelegatingAppender.DEFAULT_APPENDER);
assertThat(((CompositeAppender) compositeAppender).getAppenderTwo()).isEqualTo(stringAppender);
}
@Test
public void constructStringAppender() {
StringAppender.StringAppenderWrapper mockWrapper = mock(StringAppender.StringAppenderWrapper.class);
StringAppender appender = new StringAppender(mockWrapper);
assertThat(appender).isNotNull();
assertThat(appender.getStringAppenderWrapper()).isEqualTo(mockWrapper);
}
@Test(expected = IllegalArgumentException.class)
public void constructStringAppenderWithNullWrapper() {
try {
new StringAppender(null);
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("StringAppenderWrapper must not be null");
assertThat(expected).hasNoCause();
throw expected;
}
}
@Test
public void appendCallsConfiguredStringAppenderWrapper() {
ILoggingEvent mockEvent = mock(ILoggingEvent.class);
StringAppender appender = spy(new StringAppender(this.mockWrapper));
doReturn(this.mockWrapper).when(appender).getStringAppenderWrapper();
doReturn("TEST").when(appender).toString(eq(mockEvent));
appender.append(mockEvent);
verify(this.mockWrapper, times(1)).append(eq("TEST"));
}
@Test
public void appendIgnoresBlankEmptyAndNullStrings() {
ILoggingEvent mockEvent = mock(ILoggingEvent.class);
StringAppender appender = spy(new StringAppender(this.mockWrapper));
doReturn(this.mockWrapper).when(appender).getStringAppenderWrapper();
doReturn(" ").doReturn("").doReturn(null)
.when(appender).toString(eq(mockEvent));
appender.append(mockEvent);
appender.append(mockEvent);
appender.append(mockEvent);
verify(appender, times(3)).toString(eq(mockEvent));
verify(appender, times(1)).preProcessLogMessage(eq(" "));
verify(appender, times(1)).preProcessLogMessage(eq(""));
verify(appender, times(1)).preProcessLogMessage(eq(null));
verify(this.mockWrapper, never()).append(any());
}
@Test
public void appendIsNullSafe() {
StringAppender appender = spy(new StringAppender(this.mockWrapper));
doReturn(this.mockWrapper).when(appender).getStringAppenderWrapper();
appender.append(null);
verify(appender, never()).toString(any(ILoggingEvent.class));
verify(this.mockWrapper, never()).append(any());
}
@Test
public void emptyStringIsNotValidLogMessage() {
StringAppender appender = new StringAppender(this.mockWrapper);
assertThat(appender.isValidLogMessage("")).isFalse();
assertThat(appender.isValidLogMessage(null)).isFalse(); }
@Test
public void nonEmptyStringIsValidLogMessage() {
StringAppender appender = new StringAppender(this.mockWrapper);
assertThat(appender.isValidLogMessage("TEST")).isTrue();
assertThat(appender.isValidLogMessage("_")).isTrue();
assertThat(appender.isValidLogMessage(" ")).isTrue();
}
@Test
public void preProcessLogMessageIsNullSafe() {
assertThat(new StringAppender(this.mockWrapper).preProcessLogMessage(null)).isNull();
}
@Test
public void preProcessLogMessageTrimsMessage() {
StringAppender appender = new StringAppender(this.mockWrapper);
assertThat(appender.preProcessLogMessage("TEST")).isEqualTo("TEST");
assertThat(appender.preProcessLogMessage(" MOCK ")).isEqualTo("MOCK");
assertThat(appender.preProcessLogMessage("J UNK ")).isEqualTo("J UNK");
}
@Test
public void toStringCallsLoggingEventFormattedMessage() {
ILoggingEvent mockEvent = mock(ILoggingEvent.class);
when(mockEvent.getFormattedMessage()).thenReturn("TEST");
assertThat(new StringAppender(this.mockWrapper).toString(mockEvent)).isEqualTo("TEST");
verify(mockEvent, times(1)).getFormattedMessage();
}
@Test
public void toStringIsNullSafe() {
assertThat(new StringAppender(this.mockWrapper).toString(null)).isNull();
}
@Test
public void getLogOutputCallsStringAppenderWrapperToString() {
doReturn("TEST").when(this.mockWrapper).toString();
assertThat(new StringAppender(this.mockWrapper).getLogOutput()).isEqualTo("TEST");
}
}