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..1cdae7dc5e5 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/LoggersEndpoint.java @@ -0,0 +1,92 @@ +/* + * 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 + * @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 loggerConfigurations = this.loggingSystem + .listLoggerConfigurations(); + + if (loggerConfigurations == null) { + return Collections.emptyMap(); + } + + Map> result = new LinkedHashMap>(loggerConfigurations.size()); + + for (LoggerConfiguration loggerConfiguration : loggerConfigurations) { + result.put(loggerConfiguration.getName(), result(loggerConfiguration)); + } + + return result; + } + + public Map get(String name) { + Assert.notNull(name, "Name must not be null"); + return result(this.loggingSystem.getLoggerConfiguration(name)); + } + + public void set(String name, LogLevel level) { + Assert.notNull(name, "Name must not be empty"); + this.loggingSystem.setLogLevel(name, level); + } + + private static Map result(LoggerConfiguration loggerConfiguration) { + if (loggerConfiguration == null) { + return Collections.emptyMap(); + } + Map result = new LinkedHashMap(3); + LogLevel configuredLevel = loggerConfiguration.getConfiguredLevel(); + result.put("configuredLevel", + configuredLevel != null ? configuredLevel.name() : null); + result.put("effectiveLevel", loggerConfiguration.getEffectiveLevel().name()); + return result; + } + +} 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..c0fac73af71 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpoint.java @@ -0,0 +1,76 @@ +/* + * 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.context.properties.ConfigurationProperties; +import org.springframework.boot.logging.LogLevel; +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(); + } + return this.delegate.get(name); + } + + @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.set(name, level == null ? null : LogLevel.valueOf(level)); + return ResponseEntity.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..31044b0b02c 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,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; @@ -52,6 +53,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 +77,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 +92,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 +125,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 +256,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..53572d4a073 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/LoggersEndpointTests.java @@ -0,0 +1,90 @@ +/* + * 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 java.util.Map; + +import org.junit.Test; + +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 invoke() throws Exception { + given(getLoggingSystem().listLoggerConfigurations()).willReturn(Collections + .singleton(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG))); + Map loggingConfiguration = getEndpointBean().invoke() + .get("ROOT"); + assertThat(loggingConfiguration.get("configuredLevel")).isNull(); + assertThat(loggingConfiguration.get("effectiveLevel")).isEqualTo("DEBUG"); + } + + public void get() throws Exception { + given(getLoggingSystem().getLoggerConfiguration("ROOT")) + .willReturn(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG)); + Map loggingConfiguration = getEndpointBean().get("ROOT"); + assertThat(loggingConfiguration.get("configuredLevel")).isNull(); + assertThat(loggingConfiguration.get("effectiveLevel")).isEqualTo("DEBUG"); + } + + public void set() throws Exception { + getEndpointBean().set("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..8ada13d1126 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java @@ -0,0 +1,147 @@ +/* + * 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.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +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.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +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 + */ +@RunWith(SpringRunner.class) +@DirtiesContext +@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).build(); + } + + @Test + public void list() throws Exception { + given(this.loggingSystem.listLoggerConfigurations()).willReturn(Collections + .singleton(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 listDisabled() throws Exception { + this.context.getBean(LoggersEndpoint.class).setEnabled(false); + this.mvc.perform(get("/loggers")).andExpect(status().isNotFound()); + } + + @Test + public void getLogger() 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 getLoggerDisabled() throws Exception { + this.context.getBean(LoggersEndpoint.class).setEnabled(false); + this.mvc.perform(get("/loggers/ROOT")).andExpect(status().isNotFound()); + } + + @Test + public void setLogger() 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 setLoggerDisabled() 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); + } + + @Import({ JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, WebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class }) + @Configuration + 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/LoggerConfiguration.java b/spring-boot/src/main/java/org/springframework/boot/logging/LoggerConfiguration.java new file mode 100644 index 00000000000..7b695c25208 --- /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 class LoggerConfiguration { + + private final LogLevel configuredLevel; + + private final LogLevel effectiveLevel; + + private final String name; + + /** + * 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..d40b7478acc --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/logging/LoggerConfigurationComparator.java @@ -0,0 +1,56 @@ +/* + * 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..f23e7047f09 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 @@ -16,6 +16,7 @@ package org.springframework.boot.logging; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -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 { @@ -92,6 +94,26 @@ public abstract class LoggingSystem { return null; } + /** + * Returns the current configuration for a {@link LoggingSystem}'s logger. + * @param loggerName the name of the logger + * @return the current configuration + */ + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + throw new UnsupportedOperationException( + "Getting a logger configuration is not supported"); + } + + /** + * Returns a collection of the current configuration for all a {@link LoggingSystem}'s + * loggers. + * @return the current configurations + */ + public Collection listLoggerConfigurations() { + throw new UnsupportedOperationException( + "Listing logger configurations is not supported"); + } + /** * Sets the logging level for a given logger. * @param loggerName the name of the logger to set @@ -141,6 +163,16 @@ public abstract class LoggingSystem { } + @Override + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + return null; + } + + @Override + public Collection listLoggerConfigurations() { + return Collections.emptyList(); + } + @Override public void setLogLevel(String loggerName, LogLevel level) { 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..74a09cb7ea1 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,8 +18,12 @@ package org.springframework.boot.logging.java; import java.io.ByteArrayInputStream; import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.Enumeration; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.LogManager; @@ -28,6 +32,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,11 +47,17 @@ 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 LoggerConfigurationComparator COMPARATOR = + new LoggerConfigurationComparator(""); + private static final Map LEVELS; + private static final Map LOG_LEVELS; + static { Map levels = new HashMap(); levels.put(LogLevel.TRACE, Level.FINEST); @@ -56,6 +68,12 @@ public class JavaLoggingSystem extends AbstractLoggingSystem { levels.put(LogLevel.FATAL, Level.SEVERE); levels.put(LogLevel.OFF, Level.OFF); LEVELS = Collections.unmodifiableMap(levels); + + Map logLevels = new HashMap(); + for (Map.Entry entry : LEVELS.entrySet()) { + logLevels.put(entry.getValue(), entry.getKey()); + } + LOG_LEVELS = Collections.unmodifiableMap(logLevels); } public JavaLoggingSystem(ClassLoader classLoader) { @@ -108,6 +126,24 @@ public class JavaLoggingSystem extends AbstractLoggingSystem { } } + @Override + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + return toLoggerConfiguration(Logger.getLogger(loggerName)); + } + + @Override + public Collection listLoggerConfigurations() { + List result = new ArrayList(); + for (Enumeration loggerNames = + LogManager.getLogManager().getLoggerNames(); + loggerNames.hasMoreElements(); ) { + result.add(toLoggerConfiguration(Logger.getLogger( + loggerNames.nextElement()))); + } + Collections.sort(result, COMPARATOR); + return result; + } + @Override public void setLogLevel(String loggerName, LogLevel level) { Assert.notNull(level, "Level must not be null"); @@ -121,6 +157,20 @@ public class JavaLoggingSystem extends AbstractLoggingSystem { return new ShutdownHandler(); } + private static LoggerConfiguration toLoggerConfiguration(Logger logger) { + return new LoggerConfiguration(logger.getName(), + LOG_LEVELS.get(logger.getLevel()), + LOG_LEVELS.get(getEffectiveLevel(logger))); + } + + private static Level getEffectiveLevel(Logger root) { + Logger logger = root; + while (logger.getLevel() == null) { + logger = logger.getParent(); + } + return logger.getLevel(); + } + private final class ShutdownHandler implements Runnable { @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..9e232d65696 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 @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -40,6 +41,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,14 +57,20 @@ 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 Map LOG_LEVELS; + static { Map levels = new HashMap(); levels.put(LogLevel.TRACE, Level.TRACE); @@ -72,6 +81,12 @@ public class Log4J2LoggingSystem extends Slf4JLoggingSystem { levels.put(LogLevel.FATAL, Level.FATAL); levels.put(LogLevel.OFF, Level.OFF); LEVELS = Collections.unmodifiableMap(levels); + + Map logLevels = new HashMap(); + for (Map.Entry entry : LEVELS.entrySet()) { + logLevels.put(entry.getValue(), entry.getKey()); + } + LOG_LEVELS = Collections.unmodifiableMap(logLevels); } private static final Filter FILTER = new AbstractFilter() { @@ -194,6 +209,22 @@ public class Log4J2LoggingSystem extends Slf4JLoggingSystem { getLoggerContext().reconfigure(); } + @Override + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + return toLoggerConfiguration(getLoggerConfig(loggerName)); + } + + @Override + public Collection listLoggerConfigurations() { + List result = new ArrayList(); + for (LoggerConfig loggerConfig : + getLoggerContext().getConfiguration().getLoggers().values()) { + result.add(toLoggerConfiguration(loggerConfig)); + } + Collections.sort(result, COMPARATOR); + return result; + } + @Override public void setLogLevel(String loggerName, LogLevel logLevel) { Level level = LEVELS.get(logLevel); @@ -241,6 +272,12 @@ public class Log4J2LoggingSystem extends Slf4JLoggingSystem { loggerContext.setExternalContext(null); } + private static LoggerConfiguration toLoggerConfiguration(LoggerConfig loggerConfig) { + return new LoggerConfiguration(loggerConfig.getName(), + LOG_LEVELS.get(loggerConfig.getLevel()), + LOG_LEVELS.get(loggerConfig.getLevel())); + } + private final class ShutdownHandler implements Runnable { @Override 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..f32cf80b9b7 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,6 +19,8 @@ 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.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -40,6 +42,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,13 +57,19 @@ 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 Map LOG_LEVELS; + static { Map levels = new HashMap(); levels.put(LogLevel.TRACE, Level.TRACE); @@ -70,6 +80,12 @@ public class LogbackLoggingSystem extends Slf4JLoggingSystem { levels.put(LogLevel.FATAL, Level.ERROR); levels.put(LogLevel.OFF, Level.OFF); LEVELS = Collections.unmodifiableMap(levels); + + Map logLevels = new HashMap(); + for (Map.Entry entry : LEVELS.entrySet()) { + logLevels.put(entry.getValue(), entry.getKey()); + } + LOG_LEVELS = Collections.unmodifiableMap(logLevels); } private static final TurboFilter FILTER = new TurboFilter() { @@ -209,6 +225,21 @@ public class LogbackLoggingSystem extends Slf4JLoggingSystem { System.setProperty("org.jboss.logging.provider", "slf4j"); } + @Override + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + return toLoggerConfiguration(getLogger(loggerName)); + } + + @Override + public Collection listLoggerConfigurations() { + List result = new ArrayList(); + for (ch.qos.logback.classic.Logger logger : getLoggerContext().getLoggerList()) { + result.add(toLoggerConfiguration(logger)); + } + Collections.sort(result, COMPARATOR); + return result; + } + @Override public void setLogLevel(String loggerName, LogLevel level) { getLogger(loggerName).setLevel(LEVELS.get(level)); @@ -265,6 +296,13 @@ public class LogbackLoggingSystem extends Slf4JLoggingSystem { loggerContext.removeObject(LoggingSystem.class.getName()); } + private static LoggerConfiguration toLoggerConfiguration( + ch.qos.logback.classic.Logger logger) { + return new LoggerConfiguration(logger.getName(), + LOG_LEVELS.get(logger.getLevel()), + LOG_LEVELS.get(logger.getEffectiveLevel())); + } + private final class ShutdownHandler implements Runnable { @Override 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..1a3cb4f765a --- /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..b6c6ff24650 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.Collection; 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 { @@ -516,6 +518,16 @@ public class LoggingApplicationListenerTests { LogFile logFile) { } + @Override + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + return null; + } + + @Override + public Collection listLoggerConfigurations() { + return null; + } + @Override public void setLogLevel(String loggerName, LogLevel level) { @@ -560,6 +572,16 @@ public class LoggingApplicationListenerTests { } + @Override + public LoggerConfiguration getLoggerConfiguration(String loggerName) { + return null; + } + + @Override + public Collection listLoggerConfigurations() { + return null; + } + @Override public void setLogLevel(String loggerName, LogLevel level) { 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..735e5814a4f 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().listLoggerConfigurations(); + } + + 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..a624c50b052 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.Collection; 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(); @@ -139,6 +148,27 @@ public class JavaLoggingSystemTests extends AbstractLoggingSystemTests { null); } + @Test + public void getLoggingConfiguration() throws Exception { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(null, null, null); + this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); + assertThat(this.loggingSystem.getLoggerConfiguration(getClass().getName())) + .isEqualTo(new LoggerConfiguration(getClass().getName(), + LogLevel.DEBUG, LogLevel.DEBUG)); + } + + @Test + public void listLoggingConfigurations() throws Exception { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(null, null, null); + this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); + Collection loggerConfigurations = this.loggingSystem + .listLoggerConfigurations(); + assertThat(loggerConfigurations.size()).isGreaterThan(0); + assertThat(loggerConfigurations.iterator().next().getName()).isEmpty(); + } + @Test public void setLevel() throws Exception { this.loggingSystem.beforeInitialize(); 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..25de8b8f318 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 @@ -21,6 +21,7 @@ import java.beans.PropertyChangeListener; import java.io.File; import java.io.FileReader; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; @@ -38,6 +39,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 +59,7 @@ import static org.mockito.Mockito.verify; * @author Daniel Fullarton * @author Phillip Webb * @author Andy Wilkinson + * @author Ben Hale */ public class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @@ -120,6 +123,27 @@ public class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { this.loggingSystem.initialize(null, "classpath:log4j2-nonexistent.xml", null); } + @Test + public void getLoggingConfiguration() throws Exception { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(null, null, null); + this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); + assertThat(this.loggingSystem.getLoggerConfiguration(getClass().getName())) + .isEqualTo(new LoggerConfiguration(getClass().getName(), + LogLevel.DEBUG, LogLevel.DEBUG)); + } + + @Test + public void listLoggingConfigurations() throws Exception { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(null, null, null); + this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); + Collection loggerConfigurations = this.loggingSystem + .listLoggerConfigurations(); + assertThat(loggerConfigurations.size()).isGreaterThan(0); + assertThat(loggerConfigurations.iterator().next().getName()).isEmpty(); + } + @Test public void setLevel() throws Exception { this.loggingSystem.beforeInitialize(); 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..83a8fd60cb1 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.Collection; 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 { @@ -159,6 +162,28 @@ public class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { "classpath:logback-nonexistent.xml", null); } + @Test + public void getLoggingConfiguration() throws Exception { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); + assertThat(this.loggingSystem.getLoggerConfiguration(getClass().getName())) + .isEqualTo(new LoggerConfiguration(getClass().getName(), + LogLevel.DEBUG, LogLevel.DEBUG)); + } + + @Test + public void listLoggingConfigurations() throws Exception { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); + Collection loggerConfigurations = this.loggingSystem + .listLoggerConfigurations(); + assertThat(loggerConfigurations.size()).isGreaterThan(0); + assertThat(loggerConfigurations.iterator().next().getName()).isEqualTo( + org.slf4j.Logger.ROOT_LOGGER_NAME); + } + @Test public void setLevel() throws Exception { this.loggingSystem.beforeInitialize();