From 932c3c206bc27f99669645cadac526e4e1171e53 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 27 Jan 2014 13:48:52 -0800 Subject: [PATCH] Add LevelRemappingAppender and remap thymeleaf Add a LevelRemappingAppender that can remap the level of logback events as they are written. Also update the base configuration to change the somewhat noisy Thymeleaf INFO logging to DEBUG. Fixes gh-265 --- .../logback/LevelRemappingAppender.java | 201 ++++++++++++++++++ .../boot/logging/logback/base.xml | 7 + .../logback/LevelRemappingAppenderTests.java | 103 +++++++++ 3 files changed, 311 insertions(+) create mode 100644 spring-boot/src/main/java/org/springframework/boot/logging/logback/LevelRemappingAppender.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/logging/logback/LevelRemappingAppenderTests.java diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/logback/LevelRemappingAppender.java b/spring-boot/src/main/java/org/springframework/boot/logging/logback/LevelRemappingAppender.java new file mode 100644 index 0000000000..bbbf3cc6e1 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/logging/logback/LevelRemappingAppender.java @@ -0,0 +1,201 @@ +/* + * Copyright 2012-2014 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 + * + * http://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.boot.logging.logback; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Marker; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.LoggerContextVO; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.AppenderBase; + +/** + * {@link Appender} that can remap {@link ILoggingEvent} {@link Level}s as they are + * written. + * + * @author Phillip Webb + * @see #setRemapLevels(String) + * @see #setDestinationLogger(String) + */ +public class LevelRemappingAppender extends AppenderBase { + + private static final Map DEFAULT_REMAPS = Collections.singletonMap( + Level.INFO, Level.DEBUG); + + private String destinationLogger = Logger.ROOT_LOGGER_NAME; + + private Map remapLevels = DEFAULT_REMAPS; + + @Override + protected void append(ILoggingEvent event) { + Level remappedLevel = this.remapLevels.get(event.getLevel()); + if (remappedLevel != null) { + AppendableLogger logger = getLogger(this.destinationLogger); + logger.callAppenders(new RemappedLoggingEvent(event)); + } + } + + protected AppendableLogger getLogger(String name) { + LoggerContext loggerContext = (LoggerContext) this.context; + return new AppendableLogger(loggerContext.getLogger(name)); + } + + /** + * Sets the destination logger that will be used to send remapped events. If not + * specified the root logger is used. + * @param destinationLogger the destinationLogger name + */ + public void setDestinationLogger(String destinationLogger) { + Assert.hasLength(destinationLogger, "DestinationLogger must not be empty"); + this.destinationLogger = destinationLogger; + } + + /** + * Set the remapped level. + * @param remapLevels Comma separated String of remapped levels in the form + * {@literal "FROM->TO"}. For example, {@literal "DEBUG->TRACE,ERROR->WARN"}. + */ + public void setRemapLevels(String remapLevels) { + Assert.hasLength(remapLevels, "RemapLevels must not be empty"); + this.remapLevels = new HashMap(); + for (String remap : StringUtils.commaDelimitedListToStringArray(remapLevels)) { + String[] split = StringUtils.split(remap, "->"); + Assert.notNull(split, "Remap element '" + remap + "' must contain '->'"); + this.remapLevels.put(Level.toLevel(split[0]), Level.toLevel(split[1])); + } + } + + /** + * Simple wrapper around a logger that can have events appended. + */ + protected static class AppendableLogger { + + private Logger logger; + + public AppendableLogger(Logger logger) { + this.logger = logger; + } + + public void callAppenders(ILoggingEvent event) { + if (this.logger.isEnabledFor(event.getLevel())) { + this.logger.callAppenders(event); + } + } + } + + /** + * Decorate an existing {@link ILoggingEvent} changing the level to DEBUG. + */ + private class RemappedLoggingEvent implements ILoggingEvent { + + private final ILoggingEvent event; + + public RemappedLoggingEvent(ILoggingEvent event) { + this.event = event; + } + + @Override + public String getThreadName() { + return this.event.getThreadName(); + } + + @Override + public Level getLevel() { + Level remappedLevel = LevelRemappingAppender.this.remapLevels.get(this.event + .getLevel()); + return (remappedLevel == null ? this.event.getLevel() : remappedLevel); + } + + @Override + public String getMessage() { + return this.event.getMessage(); + } + + @Override + public Object[] getArgumentArray() { + return this.event.getArgumentArray(); + } + + @Override + public String getFormattedMessage() { + return this.event.getFormattedMessage(); + } + + @Override + public String getLoggerName() { + return this.event.getLoggerName(); + } + + @Override + public LoggerContextVO getLoggerContextVO() { + return this.event.getLoggerContextVO(); + } + + @Override + public IThrowableProxy getThrowableProxy() { + return this.event.getThrowableProxy(); + } + + @Override + public StackTraceElement[] getCallerData() { + return this.event.getCallerData(); + } + + @Override + public boolean hasCallerData() { + return this.event.hasCallerData(); + } + + @Override + public Marker getMarker() { + return this.event.getMarker(); + } + + @Override + public Map getMDCPropertyMap() { + return this.event.getMDCPropertyMap(); + } + + @Override + @Deprecated + public Map getMdc() { + return this.event.getMdc(); + } + + @Override + public long getTimeStamp() { + return this.event.getTimeStamp(); + } + + @Override + public void prepareForDeferredProcessing() { + this.event.prepareForDeferredProcessing(); + } + + } + +} diff --git a/spring-boot/src/main/resources/org/springframework/boot/logging/logback/base.xml b/spring-boot/src/main/resources/org/springframework/boot/logging/logback/base.xml index 18ae3e537b..db049948ce 100644 --- a/spring-boot/src/main/resources/org/springframework/boot/logging/logback/base.xml +++ b/spring-boot/src/main/resources/org/springframework/boot/logging/logback/base.xml @@ -28,6 +28,10 @@ + + org.springframework.boot + + @@ -38,5 +42,8 @@ + + + diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/logback/LevelRemappingAppenderTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/logback/LevelRemappingAppenderTests.java new file mode 100644 index 0000000000..d9c1ee055d --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/logging/logback/LevelRemappingAppenderTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-2014 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 + * + * http://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.boot.logging.logback; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.logging.logback.LevelRemappingAppender.AppendableLogger; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link LevelRemappingAppender}. + * + * @author Phillip Webb + */ +public class LevelRemappingAppenderTests { + + private TestableLevelRemappingAppender appender; + + @Mock + private AppendableLogger logger; + + @Captor + private ArgumentCaptor logCaptor; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + this.appender = spy(new TestableLevelRemappingAppender()); + } + + @Test + public void useRootLoggerIfNoDestination() throws Exception { + this.appender.append(mockLogEvent(Level.INFO)); + verify(this.appender).getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + } + + @Test + public void useSpecificDestination() throws Exception { + this.appender.setDestinationLogger("org.mine"); + this.appender.append(mockLogEvent(Level.INFO)); + verify(this.appender).getLogger("org.mine"); + } + + @Test + public void defaltRemapsInfo() throws Exception { + this.appender.append(mockLogEvent(Level.INFO)); + verify(this.logger).callAppenders(this.logCaptor.capture()); + assertThat(this.logCaptor.getValue().getLevel(), equalTo(Level.DEBUG)); + } + + @Test + public void customRemaps() throws Exception { + this.appender.setRemapLevels("DEBUG->TRACE,ERROR->WARN"); + this.appender.append(mockLogEvent(Level.DEBUG)); + this.appender.append(mockLogEvent(Level.ERROR)); + verify(this.logger, times(2)).callAppenders(this.logCaptor.capture()); + assertThat(this.logCaptor.getAllValues().get(0).getLevel(), equalTo(Level.TRACE)); + assertThat(this.logCaptor.getAllValues().get(1).getLevel(), equalTo(Level.WARN)); + } + + private ILoggingEvent mockLogEvent(Level level) { + ILoggingEvent event = mock(ILoggingEvent.class); + given(event.getLevel()).willReturn(level); + return event; + } + + private class TestableLevelRemappingAppender extends LevelRemappingAppender { + + @Override + protected AppendableLogger getLogger(String name) { + return LevelRemappingAppenderTests.this.logger; + } + + } +}