diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java index dbda6d4d41e..909fd041fa6 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.boot.actuate.endpoint.FlywayEndpoint; import org.springframework.boot.actuate.endpoint.HealthEndpoint; import org.springframework.boot.actuate.endpoint.InfoEndpoint; import org.springframework.boot.actuate.endpoint.LiquibaseEndpoint; +import org.springframework.boot.actuate.endpoint.LoggersEndpoint; import org.springframework.boot.actuate.endpoint.MetricsEndpoint; import org.springframework.boot.actuate.endpoint.PublicMetrics; import org.springframework.boot.actuate.endpoint.RequestMappingEndpoint; @@ -59,6 +60,7 @@ import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.logging.LoggingSystem; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.AnnotationAwareOrderComparator; @@ -75,7 +77,7 @@ import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping; * @author Stephane Nicoll * @author Eddú Meléndez * @author Meang Akira Tanaka - * + * @author Ben Hale */ @Configuration @AutoConfigureAfter({ FlywayAutoConfiguration.class, LiquibaseAutoConfiguration.class }) @@ -135,6 +137,13 @@ public class EndpointAutoConfiguration { ? Collections.emptyList() : this.infoContributors); } + @Bean + @ConditionalOnBean(LoggingSystem.class) + @ConditionalOnMissingBean + public LoggersEndpoint loggersEndpoint(LoggingSystem loggingSystem) { + return new LoggersEndpoint(loggingSystem); + } + @Bean @ConditionalOnMissingBean public MetricsEndpoint metricsEndpoint() { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java index d1101af2fa9..2c95a5a229e 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.boot.actuate.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; import org.springframework.boot.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.actuate.endpoint.LoggersEndpoint; import org.springframework.boot.actuate.endpoint.MetricsEndpoint; import org.springframework.boot.actuate.endpoint.ShutdownEndpoint; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; @@ -33,6 +34,7 @@ import org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HeapdumpMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.LogFileMvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; @@ -57,6 +59,7 @@ import org.springframework.web.cors.CorsConfiguration; * Configuration to expose {@link Endpoint} instances over Spring MVC. * * @author Dave Syer + * @author Ben Hale * @since 1.3.0 */ @ManagementContextConfiguration @@ -150,6 +153,13 @@ public class EndpointWebMvcManagementContextConfiguration { return healthMvcEndpoint; } + @Bean + @ConditionalOnBean(LoggersEndpoint.class) + @ConditionalOnEnabledEndpoint("loggers") + public LoggersMvcEndpoint loggersMvcEndpoint(LoggersEndpoint delegate) { + return new LoggersMvcEndpoint(delegate); + } + @Bean @ConditionalOnBean(MetricsEndpoint.class) @ConditionalOnEnabledEndpoint("metrics") diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/LoggersEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/LoggersEndpoint.java new file mode 100644 index 00000000000..334d5782957 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/LoggersEndpoint.java @@ -0,0 +1,108 @@ +/* + * Copyright 2016-2016 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.actuate.endpoint; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggerConfiguration; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.util.Assert; + +/** + * {@link Endpoint} to expose a collection of {@link LoggerConfiguration}s. + * + * @author Ben Hale + * @author Phillip Webb + * @since 1.5.0 + */ +@ConfigurationProperties(prefix = "endpoints.loggers") +public class LoggersEndpoint + extends AbstractEndpoint> { + + private final LoggingSystem loggingSystem; + + /** + * Create a new {@link LoggersEndpoint} instance. + * @param loggingSystem the logging system to expose + */ + public LoggersEndpoint(LoggingSystem loggingSystem) { + super("loggers"); + Assert.notNull(loggingSystem, "LoggingSystem must not be null"); + this.loggingSystem = loggingSystem; + } + + @Override + public Map invoke() { + Collection configurations = this.loggingSystem + .getLoggerConfigurations(); + if (configurations == null) { + return Collections.emptyMap(); + } + Map result = new LinkedHashMap( + configurations.size()); + for (LoggerConfiguration configuration : configurations) { + result.put(configuration.getName(), new LoggerLevels(configuration)); + } + return result; + } + + public LoggerLevels invoke(String name) { + Assert.notNull(name, "Name must not be null"); + LoggerConfiguration configuration = this.loggingSystem + .getLoggerConfiguration(name); + return (configuration == null ? null : new LoggerLevels(configuration)); + } + + public void setLogLevel(String name, LogLevel level) { + Assert.notNull(name, "Name must not be empty"); + this.loggingSystem.setLogLevel(name, level); + } + + /** + * Levels configured for a given logger exposed in a JSON friendly way. + */ + public static class LoggerLevels { + + private String configuredLevel; + + private String effectiveLevel; + + public LoggerLevels(LoggerConfiguration configuration) { + this.configuredLevel = getName(configuration.getConfiguredLevel()); + this.effectiveLevel = getName(configuration.getEffectiveLevel()); + } + + private String getName(LogLevel level) { + return (level == null ? null : level.name()); + } + + public String getConfiguredLevel() { + return this.configuredLevel; + } + + public String getEffectiveLevel() { + return this.effectiveLevel; + } + + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpoint.java new file mode 100644 index 00000000000..fc635a10ed3 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpoint.java @@ -0,0 +1,78 @@ +/* + * Copyright 2016-2016 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.actuate.endpoint.mvc; + +import java.util.Map; + +import org.springframework.boot.actuate.endpoint.LoggersEndpoint; +import org.springframework.boot.actuate.endpoint.LoggersEndpoint.LoggerLevels; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.logging.LogLevel; +import org.springframework.http.HttpEntity; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * Adapter to expose {@link LoggersEndpoint} as an {@link MvcEndpoint}. + * + * @author Ben Hale + * @since 1.5.0 + */ +@ConfigurationProperties(prefix = "endpoints.loggers") +public class LoggersMvcEndpoint extends EndpointMvcAdapter { + + private final LoggersEndpoint delegate; + + public LoggersMvcEndpoint(LoggersEndpoint delegate) { + super(delegate); + this.delegate = delegate; + } + + @GetMapping(value = "/{name:.*}", produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + @HypermediaDisabled + public Object get(@PathVariable String name) { + if (!this.delegate.isEnabled()) { + // Shouldn't happen - MVC endpoint shouldn't be registered when delegate's + // disabled + return getDisabledResponse(); + } + LoggerLevels levels = this.delegate.invoke(name); + return (levels == null ? ResponseEntity.notFound().build() : levels); + } + + @PostMapping(value = "/{name:.*}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + @HypermediaDisabled + public Object set(@PathVariable String name, + @RequestBody Map configuration) { + if (!this.delegate.isEnabled()) { + // Shouldn't happen - MVC endpoint shouldn't be registered when delegate's + // disabled + return getDisabledResponse(); + } + String level = configuration.get("configuredLevel"); + this.delegate.setLogLevel(name, level == null ? null : LogLevel.valueOf(level)); + return HttpEntity.EMPTY; + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfigurationTests.java index e4d9c0f9fee..e41fc55a8c7 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfigurationTests.java @@ -36,6 +36,8 @@ import org.springframework.boot.actuate.endpoint.FlywayEndpoint; import org.springframework.boot.actuate.endpoint.HealthEndpoint; import org.springframework.boot.actuate.endpoint.InfoEndpoint; import org.springframework.boot.actuate.endpoint.LiquibaseEndpoint; +import org.springframework.boot.actuate.endpoint.LoggersEndpoint; +import org.springframework.boot.actuate.endpoint.LoggersEndpoint.LoggerLevels; import org.springframework.boot.actuate.endpoint.MetricsEndpoint; import org.springframework.boot.actuate.endpoint.PublicMetrics; import org.springframework.boot.actuate.endpoint.RequestMappingEndpoint; @@ -52,6 +54,7 @@ import org.springframework.boot.autoconfigure.info.ProjectInfoProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.bind.PropertySourcesBinder; +import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -75,6 +78,7 @@ import static org.mockito.Mockito.mock; * @author Stephane Nicoll * @author Eddú Meléndez * @author Meang Akira Tanaka + * @author Ben Hale */ public class EndpointAutoConfigurationTests { @@ -89,12 +93,13 @@ public class EndpointAutoConfigurationTests { @Test public void endpoints() throws Exception { - load(EndpointAutoConfiguration.class); + load(CustomLoggingConfig.class, EndpointAutoConfiguration.class); assertThat(this.context.getBean(BeansEndpoint.class)).isNotNull(); assertThat(this.context.getBean(DumpEndpoint.class)).isNotNull(); assertThat(this.context.getBean(EnvironmentEndpoint.class)).isNotNull(); assertThat(this.context.getBean(HealthEndpoint.class)).isNotNull(); assertThat(this.context.getBean(InfoEndpoint.class)).isNotNull(); + assertThat(this.context.getBean(LoggersEndpoint.class)).isNotNull(); assertThat(this.context.getBean(MetricsEndpoint.class)).isNotNull(); assertThat(this.context.getBean(ShutdownEndpoint.class)).isNotNull(); assertThat(this.context.getBean(TraceEndpoint.class)).isNotNull(); @@ -121,6 +126,14 @@ public class EndpointAutoConfigurationTests { assertThat(result).isNotNull(); } + @Test + public void loggersEndpointHasLoggers() throws Exception { + load(CustomLoggingConfig.class, EndpointAutoConfiguration.class); + LoggersEndpoint endpoint = this.context.getBean(LoggersEndpoint.class); + Map loggers = endpoint.invoke(); + assertThat(loggers.size()).isGreaterThan(0); + } + @Test public void metricEndpointsHasSystemMetricsByDefault() { load(PublicMetricsAutoConfiguration.class, EndpointAutoConfiguration.class); @@ -244,6 +257,16 @@ public class EndpointAutoConfigurationTests { this.context.refresh(); } + @Configuration + static class CustomLoggingConfig { + + @Bean + LoggingSystem loggingSystem() { + return LoggingSystem.get(getClass().getClassLoader()); + } + + } + @Configuration static class CustomPublicMetricsConfig { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java index 09a10ca2852..e0ffed25b46 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java @@ -49,6 +49,7 @@ import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCusto import org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.ShutdownMvcEndpoint; @@ -71,6 +72,7 @@ import org.springframework.boot.context.embedded.ServerPortInfoApplicationContex import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.undertow.UndertowEmbeddedServletContainerFactory; import org.springframework.boot.context.event.ApplicationFailedEvent; +import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.boot.testutil.Matched; import org.springframework.context.ApplicationContext; @@ -108,6 +110,7 @@ import static org.mockito.Mockito.mock; * @author Greg Turnquist * @author Andy Wilkinson * @author Eddú Meléndez + * @author Ben Hale */ public class EndpointWebMvcAutoConfigurationTests { @@ -429,12 +432,13 @@ public class EndpointWebMvcAutoConfigurationTests { @Test public void endpointsDefaultConfiguration() throws Exception { - this.applicationContext.register(RootConfig.class, BaseConfiguration.class, - ServerPortConfig.class, EndpointWebMvcAutoConfiguration.class); + this.applicationContext.register(LoggingConfig.class, RootConfig.class, + BaseConfiguration.class, ServerPortConfig.class, + EndpointWebMvcAutoConfiguration.class); this.applicationContext.refresh(); - // /health, /metrics, /env, /actuator, /heapdump (/shutdown is disabled by - // default) - assertThat(this.applicationContext.getBeansOfType(MvcEndpoint.class)).hasSize(5); + // /health, /metrics, /loggers, /env, /actuator, /heapdump (/shutdown is disabled + // by default) + assertThat(this.applicationContext.getBeansOfType(MvcEndpoint.class)).hasSize(6); } @Test @@ -457,6 +461,16 @@ public class EndpointWebMvcAutoConfigurationTests { endpointEnabledOverride("env", EnvironmentMvcEndpoint.class); } + @Test + public void loggersEndpointDisabled() throws Exception { + endpointDisabled("loggers", LoggersMvcEndpoint.class); + } + + @Test + public void loggersEndpointEnabledOverride() throws Exception { + endpointEnabledOverride("loggers", LoggersMvcEndpoint.class); + } + @Test public void metricsEndpointDisabled() throws Exception { endpointDisabled("metrics", MetricsMvcEndpoint.class); @@ -625,8 +639,9 @@ public class EndpointWebMvcAutoConfigurationTests { private void endpointEnabledOverride(String name, Class type) throws Exception { - this.applicationContext.register(RootConfig.class, BaseConfiguration.class, - ServerPortConfig.class, EndpointWebMvcAutoConfiguration.class); + this.applicationContext.register(LoggingConfig.class, RootConfig.class, + BaseConfiguration.class, ServerPortConfig.class, + EndpointWebMvcAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.applicationContext, "endpoints.enabled:false", String.format("endpoints_%s_enabled:true", name)); @@ -740,6 +755,16 @@ public class EndpointWebMvcAutoConfigurationTests { } + @Configuration + public static class LoggingConfig { + + @Bean + public LoggingSystem loggingSystem() { + return LoggingSystem.get(getClass().getClassLoader()); + } + + } + @Configuration public static class ServerPortConfig { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MvcEndpointPathConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MvcEndpointPathConfigurationTests.java index 6131ce3d6e7..049c8826357 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MvcEndpointPathConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MvcEndpointPathConfigurationTests.java @@ -42,6 +42,7 @@ import org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.JolokiaMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.LogFileMvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.ManagementServletContext; import org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; @@ -50,6 +51,7 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration; +import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -62,6 +64,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for configuring the path of an MVC endpoint. * * @author Andy Wilkinson + * @author Ben Hale */ @RunWith(Parameterized.class) public class MvcEndpointPathConfigurationTests { @@ -95,6 +98,7 @@ public class MvcEndpointPathConfigurationTests { new Object[] { "jolokia", JolokiaMvcEndpoint.class }, new Object[] { "liquibase", LiquibaseEndpoint.class }, new Object[] { "logfile", LogFileMvcEndpoint.class }, + new Object[] { "loggers", LoggersMvcEndpoint.class }, new Object[] { "mappings", RequestMappingEndpoint.class }, new Object[] { "metrics", MetricsMvcEndpoint.class }, new Object[] { "shutdown", ShutdownEndpoint.class }, @@ -151,6 +155,11 @@ public class MvcEndpointPathConfigurationTests { return ConditionEvaluationReport.get(beanFactory); } + @Bean + LoggingSystem loggingSystem() { + return LoggingSystem.get(getClass().getClassLoader()); + } + @Bean public FlywayEndpoint flyway() { return new FlywayEndpoint(new Flyway()); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/LoggersEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/LoggersEndpointTests.java new file mode 100644 index 00000000000..21e27f7b8c7 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/LoggersEndpointTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2016-2016 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.actuate.endpoint; + +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.boot.actuate.endpoint.LoggersEndpoint.LoggerLevels; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggerConfiguration; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link LoggersEndpoint}. + * + * @author Ben Hale + */ +public class LoggersEndpointTests extends AbstractEndpointTests { + + public LoggersEndpointTests() { + super(Config.class, LoggersEndpoint.class, "loggers", true, "endpoints.loggers"); + } + + @Test + public void invokeShouldReturnConfigurations() throws Exception { + given(getLoggingSystem().getLoggerConfigurations()).willReturn(Collections + .singletonList(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG))); + LoggerLevels levels = getEndpointBean().invoke().get("ROOT"); + assertThat(levels.getConfiguredLevel()).isNull(); + assertThat(levels.getEffectiveLevel()).isEqualTo("DEBUG"); + } + + public void invokeWhenNameSpecifiedShouldReturnLevels() throws Exception { + given(getLoggingSystem().getLoggerConfiguration("ROOT")) + .willReturn(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG)); + LoggerLevels levels = getEndpointBean().invoke("ROOT"); + assertThat(levels.getConfiguredLevel()).isNull(); + assertThat(levels.getEffectiveLevel()).isEqualTo("DEBUG"); + } + + public void setLogLevelShouldSetLevelOnLoggingSystem() throws Exception { + getEndpointBean().setLogLevel("ROOT", LogLevel.DEBUG); + verify(getLoggingSystem()).setLogLevel("ROOT", LogLevel.DEBUG); + } + + private LoggingSystem getLoggingSystem() { + return this.context.getBean(LoggingSystem.class); + } + + @Configuration + @EnableConfigurationProperties + public static class Config { + + @Bean + public LoggingSystem loggingSystem() { + return mock(LoggingSystem.class); + } + + @Bean + public LoggersEndpoint endpoint(LoggingSystem loggingSystem) { + return new LoggersEndpoint(loggingSystem); + } + + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java index 4afe550a14a..049c8dfbc34 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java @@ -119,7 +119,7 @@ public class HalBrowserMvcEndpointVanillaIntegrationTests { @Test public void endpointsEachHaveSelf() throws Exception { Set collections = new HashSet( - Arrays.asList("/trace", "/beans", "/dump", "/heapdump")); + Arrays.asList("/trace", "/beans", "/dump", "/heapdump", "/loggers")); for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) { String path = endpoint.getPath(); if (collections.contains(path)) { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java new file mode 100644 index 00000000000..bcde5aecc6d --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java @@ -0,0 +1,159 @@ +/* + * Copyright 2016-2016 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.actuate.endpoint.mvc; + +import java.util.Collections; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; +import org.springframework.boot.actuate.endpoint.LoggersEndpoint; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggerConfiguration; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link LoggersMvcEndpoint}. + * + * @author Ben Hale + * @author Phillip Webb + */ +@RunWith(SpringRunner.class) +@SpringBootTest +public class LoggersMvcEndpointTests { + + @Autowired + private WebApplicationContext context; + + @Autowired + private LoggingSystem loggingSystem; + + private MockMvc mvc; + + @Before + public void setUp() { + this.context.getBean(LoggersEndpoint.class).setEnabled(true); + this.mvc = MockMvcBuilders.webAppContextSetup(this.context) + .alwaysDo(MockMvcResultHandlers.print()).build(); + } + + @After + public void reset() { + Mockito.reset(this.loggingSystem); + } + + @Test + public void getLoggerShouldReturnAllLoggerConfigurations() throws Exception { + given(this.loggingSystem.getLoggerConfigurations()).willReturn(Collections + .singletonList(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG))); + this.mvc.perform(get("/loggers")).andExpect(status().isOk()) + .andExpect(content().string(equalTo("{\"ROOT\":{\"configuredLevel\":" + + "null,\"effectiveLevel\":\"DEBUG\"}}"))); + } + + @Test + public void getLoggersWhenDisabledShouldReturnNotFound() throws Exception { + this.context.getBean(LoggersEndpoint.class).setEnabled(false); + this.mvc.perform(get("/loggers")).andExpect(status().isNotFound()); + } + + @Test + public void getLoggerShouldReturnLogLevels() throws Exception { + given(this.loggingSystem.getLoggerConfiguration("ROOT")) + .willReturn(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG)); + this.mvc.perform(get("/loggers/ROOT")).andExpect(status().isOk()) + .andExpect(content().string(equalTo( + "{\"configuredLevel\":null," + "\"effectiveLevel\":\"DEBUG\"}"))); + } + + @Test + public void getLoggesWhenDisabledShouldReturnNotFound() throws Exception { + this.context.getBean(LoggersEndpoint.class).setEnabled(false); + this.mvc.perform(get("/loggers/ROOT")).andExpect(status().isNotFound()); + } + + @Test + public void getLoggersWhenLoggerNotFoundShouldReturnNotFound() throws Exception { + this.mvc.perform(get("/loggers/com.does.not.exist")) + .andExpect(status().isNotFound()); + } + + @Test + public void setLoggerShouldSetLogLevel() throws Exception { + this.mvc.perform(post("/loggers/ROOT").contentType(MediaType.APPLICATION_JSON) + .content("{\"configuredLevel\":\"DEBUG\"}")).andExpect(status().isOk()); + verify(this.loggingSystem).setLogLevel("ROOT", LogLevel.DEBUG); + } + + @Test + public void setLoggerWhenDisabledShouldReturnNotFound() throws Exception { + this.context.getBean(LoggersEndpoint.class).setEnabled(false); + this.mvc.perform(post("/loggers/ROOT").contentType(MediaType.APPLICATION_JSON) + .content("{\"configuredLevel\":\"DEBUG\"}")) + .andExpect(status().isNotFound()); + verifyZeroInteractions(this.loggingSystem); + } + + @Configuration + @Import({ JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, WebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class }) + public static class TestConfiguration { + + @Bean + public LoggingSystem loggingSystem() { + return mock(LoggingSystem.class); + } + + @Bean + public LoggersEndpoint endpoint(LoggingSystem loggingSystem) { + return new LoggersEndpoint(loggingSystem); + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java b/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java index d89f445062f..d3521e2cedf 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java @@ -16,6 +16,9 @@ package org.springframework.boot.logging; +import java.util.HashMap; +import java.util.Map; + import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; import org.springframework.util.ClassUtils; @@ -173,4 +176,34 @@ public abstract class AbstractLoggingSystem extends LoggingSystem { new LoggingSystemProperties(environment).apply(logFile); } + /** + * Maintains a mapping between native levels and {@link LogLevel}. + * @param The native level type + */ + protected static class LogLevels { + + private final Map systemToNative; + + private final Map nativeToSystem; + + public LogLevels() { + this.systemToNative = new HashMap(); + this.nativeToSystem = new HashMap(); + } + + public void map(LogLevel system, T nativeLevel) { + this.systemToNative.put(system, nativeLevel); + this.nativeToSystem.put(nativeLevel, system); + } + + public LogLevel convertNativeToSystem(T level) { + return this.nativeToSystem.get(level); + } + + public T convertSystemToNative(LogLevel level) { + return this.systemToNative.get(level); + } + + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/LoggerConfiguration.java b/spring-boot/src/main/java/org/springframework/boot/logging/LoggerConfiguration.java new file mode 100644 index 00000000000..4172df641e7 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/logging/LoggerConfiguration.java @@ -0,0 +1,111 @@ +/* + * Copyright 2016-2016 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; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Immutable class that represents the configuration of a {@link LoggingSystem}'s logger. + * + * @author Ben Hale + * @since 1.5.0 + */ +public final class LoggerConfiguration { + + private final String name; + + private final LogLevel configuredLevel; + + private final LogLevel effectiveLevel; + + /** + * Create a new {@link LoggerConfiguration instance}. + * @param name the name of the logger + * @param configuredLevel the configured level of the logger + * @param effectiveLevel the effective level of the logger + */ + public LoggerConfiguration(String name, LogLevel configuredLevel, + LogLevel effectiveLevel) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(effectiveLevel, "EffectiveLevel must not be null"); + this.name = name; + this.configuredLevel = configuredLevel; + this.effectiveLevel = effectiveLevel; + } + + /** + * Returns the configured level of the logger. + * @return the configured level of the logger + */ + public LogLevel getConfiguredLevel() { + return this.configuredLevel; + } + + /** + * Returns the effective level of the logger. + * @return the effective level of the logger + */ + public LogLevel getEffectiveLevel() { + return this.effectiveLevel; + } + + /** + * Returns the name of the logger. + * @return the name of the logger + */ + public String getName() { + return this.name; + } + + @Override + public String toString() { + return "LoggerConfiguration [name=" + this.name + ", configuredLevel=" + + this.configuredLevel + ", effectiveLevel=" + this.effectiveLevel + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ObjectUtils.nullSafeHashCode(this.name); + result = prime * result + ObjectUtils.nullSafeHashCode(this.configuredLevel); + result = prime * result + ObjectUtils.nullSafeHashCode(this.effectiveLevel); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (obj instanceof LoggerConfiguration) { + LoggerConfiguration other = (LoggerConfiguration) obj; + boolean rtn = true; + rtn &= ObjectUtils.nullSafeEquals(this.name, other.name); + rtn &= ObjectUtils.nullSafeEquals(this.configuredLevel, + other.configuredLevel); + rtn &= ObjectUtils.nullSafeEquals(this.effectiveLevel, other.effectiveLevel); + return rtn; + } + return super.equals(obj); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/LoggerConfigurationComparator.java b/spring-boot/src/main/java/org/springframework/boot/logging/LoggerConfigurationComparator.java new file mode 100644 index 00000000000..ae0cb440958 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/logging/LoggerConfigurationComparator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2016-2016 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; + +import java.util.Comparator; + +import org.springframework.util.Assert; + +/** + * An implementation of {@link Comparator} for comparing {@link LoggerConfiguration}s. + * Sorts the "root" logger as the first logger and then lexically by name after that. + * + * @author Ben Hale + * @since 1.5.0 + */ +public class LoggerConfigurationComparator implements Comparator { + + private final String rootLoggerName; + + /** + * Create a new {@link LoggerConfigurationComparator} instance. + * @param rootLoggerName the name of the "root" logger + */ + public LoggerConfigurationComparator(String rootLoggerName) { + Assert.notNull(rootLoggerName, "RootLoggerName must not be null"); + this.rootLoggerName = rootLoggerName; + } + + @Override + public int compare(LoggerConfiguration o1, LoggerConfiguration o2) { + if (this.rootLoggerName.equals(o1.getName())) { + return -1; + } + if (this.rootLoggerName.equals(o2.getName())) { + return 1; + } + return o1.getName().compareTo(o2.getName()); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java b/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java index 4664f8fde64..a2e1758037e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java @@ -18,6 +18,7 @@ package org.springframework.boot.logging; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.springframework.util.ClassUtils; @@ -29,6 +30,7 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Dave Syer * @author Andy Wilkinson + * @author Ben Hale */ public abstract class LoggingSystem { @@ -97,7 +99,29 @@ public abstract class LoggingSystem { * @param loggerName the name of the logger to set * @param level the log level */ - public abstract void setLogLevel(String loggerName, LogLevel level); + public void setLogLevel(String loggerName, LogLevel level) { + throw new UnsupportedOperationException("Unable to set log level"); + } + + /** + * Returns a collection of the current configuration for all a {@link LoggingSystem}'s + * loggers. + * @return the current configurations + * @since 1.5.0 + */ + public List getLoggerConfigurations() { + throw new UnsupportedOperationException("Unable to get logger configurations"); + } + + /** + * Returns the current configuration for a {@link LoggingSystem}'s logger. + * @param loggerName the name of the logger + * @return the current configuration + * @since 1.5.0 + */ + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + throw new UnsupportedOperationException("Unable to get logger configuration"); + } /** * Detect and return the logging system in use. Supports Logback and Java Logging. @@ -146,6 +170,16 @@ public abstract class LoggingSystem { } + @Override + public List getLoggerConfigurations() { + return Collections.emptyList(); + } + + @Override + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + return null; + } + } } diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/java/JavaLoggingSystem.java b/spring-boot/src/main/java/org/springframework/boot/logging/java/JavaLoggingSystem.java index 5730ad4d984..d23a99d337b 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/java/JavaLoggingSystem.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/java/JavaLoggingSystem.java @@ -18,9 +18,10 @@ package org.springframework.boot.logging.java; import java.io.ByteArrayInputStream; import java.io.InputStreamReader; +import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.util.Enumeration; +import java.util.List; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -28,6 +29,8 @@ import java.util.logging.Logger; import org.springframework.boot.logging.AbstractLoggingSystem; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggerConfiguration; +import org.springframework.boot.logging.LoggerConfigurationComparator; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; import org.springframework.util.Assert; @@ -41,21 +44,23 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Dave Syer * @author Andy Wilkinson + * @author Ben Hale */ public class JavaLoggingSystem extends AbstractLoggingSystem { - private static final Map LEVELS; + private static final LoggerConfigurationComparator COMPARATOR = new LoggerConfigurationComparator( + ""); + + private static final LogLevels LEVELS = new LogLevels(); static { - Map levels = new HashMap(); - levels.put(LogLevel.TRACE, Level.FINEST); - levels.put(LogLevel.DEBUG, Level.FINE); - levels.put(LogLevel.INFO, Level.INFO); - levels.put(LogLevel.WARN, Level.WARNING); - levels.put(LogLevel.ERROR, Level.SEVERE); - levels.put(LogLevel.FATAL, Level.SEVERE); - levels.put(LogLevel.OFF, Level.OFF); - LEVELS = Collections.unmodifiableMap(levels); + LEVELS.map(LogLevel.TRACE, Level.FINEST); + LEVELS.map(LogLevel.DEBUG, Level.FINE); + LEVELS.map(LogLevel.INFO, Level.INFO); + LEVELS.map(LogLevel.WARN, Level.WARNING); + LEVELS.map(LogLevel.ERROR, Level.SEVERE); + LEVELS.map(LogLevel.FATAL, Level.SEVERE); + LEVELS.map(LogLevel.OFF, Level.OFF); } public JavaLoggingSystem(ClassLoader classLoader) { @@ -113,7 +118,39 @@ public class JavaLoggingSystem extends AbstractLoggingSystem { Assert.notNull(level, "Level must not be null"); String name = (StringUtils.hasText(loggerName) ? loggerName : ""); Logger logger = Logger.getLogger(name); - logger.setLevel(LEVELS.get(level)); + if (logger != null) { + logger.setLevel(LEVELS.convertSystemToNative(level)); + } + } + + @Override + public List getLoggerConfigurations() { + List result = new ArrayList(); + Enumeration names = LogManager.getLogManager().getLoggerNames(); + while (names.hasMoreElements()) { + result.add(getLoggerConfiguration(names.nextElement())); + } + Collections.sort(result, COMPARATOR); + return Collections.unmodifiableList(result); + } + + @Override + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + Logger logger = Logger.getLogger(loggerName); + if (logger == null) { + return null; + } + LogLevel level = LEVELS.convertNativeToSystem(logger.getLevel()); + LogLevel effectiveLevel = LEVELS.convertNativeToSystem(getEffectiveLevel(logger)); + return new LoggerConfiguration(logger.getName(), level, effectiveLevel); + } + + private Level getEffectiveLevel(Logger root) { + Logger logger = root; + while (logger.getLevel() == null) { + logger = logger.getParent(); + } + return logger.getLevel(); } @Override diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java b/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java index 955fa748a08..999daf16cb2 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java @@ -21,9 +21,7 @@ import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -32,6 +30,7 @@ import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.Logger; import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationFactory; import org.apache.logging.log4j.core.config.ConfigurationSource; import org.apache.logging.log4j.core.config.LoggerConfig; @@ -40,6 +39,8 @@ import org.apache.logging.log4j.message.Message; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggerConfiguration; +import org.springframework.boot.logging.LoggerConfigurationComparator; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.Slf4JLoggingSystem; @@ -54,24 +55,26 @@ import org.springframework.util.StringUtils; * @author Daniel Fullarton * @author Andy Wilkinson * @author Alexander Heusingfeld + * @author Ben Hale * @since 1.2.0 */ public class Log4J2LoggingSystem extends Slf4JLoggingSystem { + private static final LoggerConfigurationComparator COMPARATOR = new LoggerConfigurationComparator( + LogManager.ROOT_LOGGER_NAME); + private static final String FILE_PROTOCOL = "file"; - private static final Map LEVELS; + private static final LogLevels LEVELS = new LogLevels(); static { - Map levels = new HashMap(); - levels.put(LogLevel.TRACE, Level.TRACE); - levels.put(LogLevel.DEBUG, Level.DEBUG); - levels.put(LogLevel.INFO, Level.INFO); - levels.put(LogLevel.WARN, Level.WARN); - levels.put(LogLevel.ERROR, Level.ERROR); - levels.put(LogLevel.FATAL, Level.FATAL); - levels.put(LogLevel.OFF, Level.OFF); - LEVELS = Collections.unmodifiableMap(levels); + LEVELS.map(LogLevel.TRACE, Level.TRACE); + LEVELS.map(LogLevel.DEBUG, Level.DEBUG); + LEVELS.map(LogLevel.INFO, Level.INFO); + LEVELS.map(LogLevel.WARN, Level.WARN); + LEVELS.map(LogLevel.ERROR, Level.ERROR); + LEVELS.map(LogLevel.FATAL, Level.FATAL); + LEVELS.map(LogLevel.OFF, Level.OFF); } private static final Filter FILTER = new AbstractFilter() { @@ -196,7 +199,7 @@ public class Log4J2LoggingSystem extends Slf4JLoggingSystem { @Override public void setLogLevel(String loggerName, LogLevel logLevel) { - Level level = LEVELS.get(logLevel); + Level level = LEVELS.convertSystemToNative(logLevel); LoggerConfig loggerConfig = getLoggerConfig(loggerName); if (loggerConfig == null) { loggerConfig = new LoggerConfig(loggerName, level, true); @@ -208,6 +211,30 @@ public class Log4J2LoggingSystem extends Slf4JLoggingSystem { getLoggerContext().updateLoggers(); } + @Override + public List getLoggerConfigurations() { + List result = new ArrayList(); + Configuration configuration = getLoggerContext().getConfiguration(); + for (LoggerConfig loggerConfig : configuration.getLoggers().values()) { + result.add(convertLoggerConfiguration(loggerConfig)); + } + Collections.sort(result, COMPARATOR); + return result; + } + + @Override + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + return convertLoggerConfiguration(getLoggerConfig(loggerName)); + } + + private LoggerConfiguration convertLoggerConfiguration(LoggerConfig loggerConfig) { + if (loggerConfig == null) { + return null; + } + LogLevel level = LEVELS.convertNativeToSystem(loggerConfig.getLevel()); + return new LoggerConfiguration(loggerConfig.getName(), level, level); + } + @Override public Runnable getShutdownHandler() { return new ShutdownHandler(); diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java b/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java index b681bc97293..f58ba8f57e7 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java @@ -19,10 +19,9 @@ package org.springframework.boot.logging.logback; import java.net.URL; import java.security.CodeSource; import java.security.ProtectionDomain; +import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; @@ -40,6 +39,8 @@ import org.slf4j.impl.StaticLoggerBinder; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggerConfiguration; +import org.springframework.boot.logging.LoggerConfigurationComparator; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.Slf4JLoggingSystem; @@ -53,23 +54,25 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Dave Syer * @author Andy Wilkinson + * @author Ben Hale */ public class LogbackLoggingSystem extends Slf4JLoggingSystem { + private static final LoggerConfigurationComparator COMPARATOR = new LoggerConfigurationComparator( + Logger.ROOT_LOGGER_NAME); + private static final String CONFIGURATION_FILE_PROPERTY = "logback.configurationFile"; - private static final Map LEVELS; + private static final LogLevels LEVELS = new LogLevels(); static { - Map levels = new HashMap(); - levels.put(LogLevel.TRACE, Level.TRACE); - levels.put(LogLevel.DEBUG, Level.DEBUG); - levels.put(LogLevel.INFO, Level.INFO); - levels.put(LogLevel.WARN, Level.WARN); - levels.put(LogLevel.ERROR, Level.ERROR); - levels.put(LogLevel.FATAL, Level.ERROR); - levels.put(LogLevel.OFF, Level.OFF); - LEVELS = Collections.unmodifiableMap(levels); + LEVELS.map(LogLevel.TRACE, Level.TRACE); + LEVELS.map(LogLevel.DEBUG, Level.DEBUG); + LEVELS.map(LogLevel.INFO, Level.INFO); + LEVELS.map(LogLevel.WARN, Level.WARN); + LEVELS.map(LogLevel.ERROR, Level.ERROR); + LEVELS.map(LogLevel.FATAL, Level.ERROR); + LEVELS.map(LogLevel.OFF, Level.OFF); } private static final TurboFilter FILTER = new TurboFilter() { @@ -209,9 +212,38 @@ public class LogbackLoggingSystem extends Slf4JLoggingSystem { System.setProperty("org.jboss.logging.provider", "slf4j"); } + @Override + public List getLoggerConfigurations() { + List result = new ArrayList(); + for (ch.qos.logback.classic.Logger logger : getLoggerContext().getLoggerList()) { + result.add(getLoggerConfiguration(logger)); + } + Collections.sort(result, COMPARATOR); + return result; + } + + @Override + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + return getLoggerConfiguration(getLogger(loggerName)); + } + + private LoggerConfiguration getLoggerConfiguration( + ch.qos.logback.classic.Logger logger) { + if (logger == null) { + return null; + } + LogLevel level = LEVELS.convertNativeToSystem(logger.getLevel()); + LogLevel effectiveLevel = LEVELS + .convertNativeToSystem(logger.getEffectiveLevel()); + return new LoggerConfiguration(logger.getName(), level, effectiveLevel); + } + @Override public void setLogLevel(String loggerName, LogLevel level) { - getLogger(loggerName).setLevel(LEVELS.get(level)); + ch.qos.logback.classic.Logger logger = getLogger(loggerName); + if (logger != null) { + logger.setLevel(LEVELS.convertSystemToNative(level)); + } } @Override @@ -221,8 +253,8 @@ public class LogbackLoggingSystem extends Slf4JLoggingSystem { private ch.qos.logback.classic.Logger getLogger(String name) { LoggerContext factory = getLoggerContext(); - return factory - .getLogger(StringUtils.isEmpty(name) ? Logger.ROOT_LOGGER_NAME : name); + name = (StringUtils.isEmpty(name) ? Logger.ROOT_LOGGER_NAME : name); + return factory.getLogger(name); } diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/LoggerConfigurationComparatorTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/LoggerConfigurationComparatorTests.java new file mode 100644 index 00000000000..8c7188f28c8 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/logging/LoggerConfigurationComparatorTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016-2016 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; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LoggerConfigurationComparator}. + * + * @author Ben Hale + */ +public class LoggerConfigurationComparatorTests { + + private final LoggerConfigurationComparator comparator = new LoggerConfigurationComparator( + "ROOT"); + + @Test + public void rootLoggerFirst() { + LoggerConfiguration first = new LoggerConfiguration("ROOT", null, LogLevel.OFF); + LoggerConfiguration second = new LoggerConfiguration("alpha", null, LogLevel.OFF); + assertThat(this.comparator.compare(first, second)).isLessThan(0); + } + + @Test + public void rootLoggerSecond() { + LoggerConfiguration first = new LoggerConfiguration("alpha", null, LogLevel.OFF); + LoggerConfiguration second = new LoggerConfiguration("ROOT", null, LogLevel.OFF); + assertThat(this.comparator.compare(first, second)).isGreaterThan(0); + } + + @Test + public void rootLoggerFirstEmpty() { + LoggerConfiguration first = new LoggerConfiguration("ROOT", null, LogLevel.OFF); + LoggerConfiguration second = new LoggerConfiguration("", null, LogLevel.OFF); + assertThat(this.comparator.compare(first, second)).isLessThan(0); + } + + @Test + public void rootLoggerSecondEmpty() { + LoggerConfiguration first = new LoggerConfiguration("", null, LogLevel.OFF); + LoggerConfiguration second = new LoggerConfiguration("ROOT", null, LogLevel.OFF); + assertThat(this.comparator.compare(first, second)).isGreaterThan(0); + } + + @Test + public void lexicalFirst() { + LoggerConfiguration first = new LoggerConfiguration("alpha", null, LogLevel.OFF); + LoggerConfiguration second = new LoggerConfiguration("bravo", null, LogLevel.OFF); + assertThat(this.comparator.compare(first, second)).isLessThan(0); + } + + @Test + public void lexicalSecond() { + LoggerConfiguration first = new LoggerConfiguration("bravo", null, LogLevel.OFF); + LoggerConfiguration second = new LoggerConfiguration("alpha", null, LogLevel.OFF); + assertThat(this.comparator.compare(first, second)).isGreaterThan(0); + } + + @Test + public void lexicalEqual() { + LoggerConfiguration first = new LoggerConfiguration("alpha", null, LogLevel.OFF); + LoggerConfiguration second = new LoggerConfiguration("alpha", null, LogLevel.OFF); + assertThat(this.comparator.compare(first, second)).isEqualTo(0); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/LoggingApplicationListenerTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/LoggingApplicationListenerTests.java index 6378cbee5d8..cbc2647c682 100644 --- a/spring-boot/src/test/java/org/springframework/boot/logging/LoggingApplicationListenerTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/logging/LoggingApplicationListenerTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.logging; import java.io.File; import java.io.IOException; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.logging.Handler; @@ -57,6 +58,7 @@ import static org.hamcrest.Matchers.not; * @author Phillip Webb * @author Andy Wilkinson * @author Stephane Nicoll + * @author Ben Hale */ public class LoggingApplicationListenerTests { @@ -518,7 +520,16 @@ public class LoggingApplicationListenerTests { @Override public void setLogLevel(String loggerName, LogLevel level) { + } + @Override + public List getLoggerConfigurations() { + return null; + } + + @Override + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + return null; } @Override @@ -552,17 +563,24 @@ public class LoggingApplicationListenerTests { private boolean cleanedUp = false; public TestCleanupLoggingSystem(ClassLoader classLoader) { - } @Override public void beforeInitialize() { - } @Override public void setLogLevel(String loggerName, LogLevel level) { + } + @Override + public List getLoggerConfigurations() { + return null; + } + + @Override + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + return null; } @Override diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemTests.java index 56e263c9f5d..aa3625b92b0 100644 --- a/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemTests.java @@ -42,4 +42,28 @@ public class LoggingSystemTests { assertThat(loggingSystem).isInstanceOf(NoOpLoggingSystem.class); } + @Test(expected = UnsupportedOperationException.class) + public void getLoggerConfigurationIsUnsupported() { + new StubLoggingSystem().getLoggerConfiguration("test-logger-name"); + } + + @Test(expected = UnsupportedOperationException.class) + public void listLoggerConfigurationsIsUnsupported() { + new StubLoggingSystem().getLoggerConfigurations(); + } + + private static final class StubLoggingSystem extends LoggingSystem { + + @Override + public void beforeInitialize() { + // Stub implementation + } + + @Override + public void setLogLevel(String loggerName, LogLevel level) { + // Stub implementation + } + + } + } diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/java/JavaLoggingSystemTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/java/JavaLoggingSystemTests.java index fb4799a7cb2..12eb04eb9cb 100644 --- a/spring-boot/src/test/java/org/springframework/boot/logging/java/JavaLoggingSystemTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/logging/java/JavaLoggingSystemTests.java @@ -19,7 +19,9 @@ package org.springframework.boot.logging.java; import java.io.File; import java.io.FileFilter; import java.io.IOException; +import java.util.List; import java.util.Locale; +import java.util.logging.Level; import org.apache.commons.logging.impl.Jdk14Logger; import org.junit.After; @@ -29,6 +31,7 @@ import org.junit.Test; import org.springframework.boot.logging.AbstractLoggingSystemTests; import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggerConfiguration; import org.springframework.boot.testutil.InternalOutputCapture; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -40,6 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Dave Syer * @author Phillip Webb + * @author Ben Hale */ public class JavaLoggingSystemTests extends AbstractLoggingSystemTests { @@ -74,6 +78,11 @@ public class JavaLoggingSystemTests extends AbstractLoggingSystemTests { Locale.setDefault(this.defaultLocale); } + @After + public void resetLogger() { + this.logger.getLogger().setLevel(Level.OFF); + } + @Test public void noFile() throws Exception { this.loggingSystem.beforeInitialize(); @@ -150,4 +159,26 @@ public class JavaLoggingSystemTests extends AbstractLoggingSystemTests { .isEqualTo(1); } + @Test + public void getLoggingConfigurations() throws Exception { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(null, null, null); + this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); + List configurations = this.loggingSystem + .getLoggerConfigurations(); + assertThat(configurations).isNotEmpty(); + assertThat(configurations.get(0).getName()).isEmpty(); + } + + @Test + public void getLoggingConfiguration() throws Exception { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(null, null, null); + this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); + LoggerConfiguration configuration = this.loggingSystem + .getLoggerConfiguration(getClass().getName()); + assertThat(configuration).isEqualTo(new LoggerConfiguration(getClass().getName(), + LogLevel.DEBUG, LogLevel.DEBUG)); + } + } diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java index a9eff17d1b4..1e06c7c891b 100644 --- a/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java @@ -38,6 +38,7 @@ import org.junit.Test; import org.springframework.boot.logging.AbstractLoggingSystemTests; import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggerConfiguration; import org.springframework.boot.testutil.InternalOutputCapture; import org.springframework.boot.testutil.Matched; import org.springframework.util.FileCopyUtils; @@ -57,6 +58,7 @@ import static org.mockito.Mockito.verify; * @author Daniel Fullarton * @author Phillip Webb * @author Andy Wilkinson + * @author Ben Hale */ public class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @@ -131,6 +133,28 @@ public class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { .isEqualTo(1); } + @Test + public void getLoggingConfigurations() throws Exception { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(null, null, null); + this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); + List configurations = this.loggingSystem + .getLoggerConfigurations(); + assertThat(configurations).isNotEmpty(); + assertThat(configurations.get(0).getName()).isEmpty(); + } + + @Test + public void getLoggingConfiguration() throws Exception { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(null, null, null); + this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); + LoggerConfiguration configuration = this.loggingSystem + .getLoggerConfiguration(getClass().getName()); + assertThat(configuration).isEqualTo(new LoggerConfiguration(getClass().getName(), + LogLevel.DEBUG, LogLevel.DEBUG)); + } + @Test public void setLevelOfUnconfiguredLoggerDoesNotAffectRootConfiguration() throws Exception { diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java index 39b4d9810a6..072b9b82108 100644 --- a/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.logging.logback; import java.io.File; import java.io.FileReader; +import java.util.List; import java.util.logging.Handler; import java.util.logging.LogManager; @@ -39,6 +40,7 @@ import org.slf4j.impl.StaticLoggerBinder; import org.springframework.boot.logging.AbstractLoggingSystemTests; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggerConfiguration; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.testutil.InternalOutputCapture; import org.springframework.boot.testutil.Matched; @@ -59,6 +61,7 @@ import static org.mockito.Mockito.verify; * @author Dave Syer * @author Phillip Webb * @author Andy Wilkinson + * @author Ben Hale */ public class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { @@ -170,6 +173,29 @@ public class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { .isEqualTo(1); } + @Test + public void getLoggingConfigurations() throws Exception { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); + List configurations = this.loggingSystem + .getLoggerConfigurations(); + assertThat(configurations).isNotEmpty(); + assertThat(configurations.get(0).getName()) + .isEqualTo(org.slf4j.Logger.ROOT_LOGGER_NAME); + } + + @Test + public void getLoggingConfiguration() throws Exception { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); + LoggerConfiguration configuration = this.loggingSystem + .getLoggerConfiguration(getClass().getName()); + assertThat(configuration).isEqualTo(new LoggerConfiguration(getClass().getName(), + LogLevel.DEBUG, LogLevel.DEBUG)); + } + @Test public void loggingThatUsesJulIsCaptured() { this.loggingSystem.beforeInitialize();