Add StringAppender Logback Appender implementation to capture log messages in-memory appended to a String.
Resolve gh-73.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user