From b9047c22e06d2955505be5de2b7fa693bab39fa4 Mon Sep 17 00:00:00 2001 From: HaiTao Zhang Date: Fri, 19 Jul 2019 18:18:07 -0700 Subject: [PATCH 1/2] Add support for configuring logging groups via endpoint See gh-17515 --- .../LoggersEndpointAutoConfiguration.java | 7 +- .../LoggersEndpointDocumentationTests.java | 56 ++++++++- .../boot/actuate/logging/LoggersEndpoint.java | 70 +++++++++-- .../actuate/logging/LoggersEndpointTests.java | 79 +++++++++++- .../LoggersEndpointWebIntegrationTests.java | 115 +++++++++++++++++- .../logging/LoggingApplicationListener.java | 28 ++++- .../boot/logging/LoggingGroups.java | 112 +++++++++++++++++ .../boot/logging/LoggingGroupsTests.java | 44 +++++++ 8 files changed, 482 insertions(+), 29 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingGroups.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingGroupsTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/LoggersEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/LoggersEndpointAutoConfiguration.java index 0b370d54959..96d419ae26c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/LoggersEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/LoggersEndpointAutoConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.logging; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.logging.LoggersEndpoint; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -24,6 +25,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.logging.LoggingGroups; import org.springframework.boot.logging.LoggingSystem; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; @@ -45,8 +47,9 @@ public class LoggersEndpointAutoConfiguration { @ConditionalOnBean(LoggingSystem.class) @Conditional(OnEnabledLoggingSystemCondition.class) @ConditionalOnMissingBean - public LoggersEndpoint loggersEndpoint(LoggingSystem loggingSystem) { - return new LoggersEndpoint(loggingSystem); + public LoggersEndpoint loggersEndpoint(LoggingSystem loggingSystem, + ObjectProvider loggingGroupsObjectProvider) { + return new LoggersEndpoint(loggingSystem, loggingGroupsObjectProvider.getIfAvailable()); } static class OnEnabledLoggingSystemCondition extends SpringBootCondition { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LoggersEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LoggersEndpointDocumentationTests.java index 8b2e18b0c8f..17ee607c844 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LoggersEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LoggersEndpointDocumentationTests.java @@ -17,14 +17,17 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation; import java.util.Arrays; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.logging.LoggersEndpoint; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; +import org.springframework.boot.logging.LoggingGroups; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; @@ -54,32 +57,60 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest fieldWithPath("configuredLevel").description("Configured level of the logger, if any.").optional(), fieldWithPath("effectiveLevel").description("Effective level of the logger.")); + private static final List groupLevelFields = Arrays.asList( + fieldWithPath("configuredLevel").description("Configured level of the logger group"), + fieldWithPath("members").description("Loggers that are part of this group").optional()); + @MockBean private LoggingSystem loggingSystem; + @MockBean + private ObjectProvider loggingGroupsObjectProvider; + + @MockBean + LoggingGroups loggingGroups; + @Test void allLoggers() throws Exception { given(this.loggingSystem.getSupportedLogLevels()).willReturn(EnumSet.allOf(LogLevel.class)); given(this.loggingSystem.getLoggerConfigurations()) .willReturn(Arrays.asList(new LoggerConfiguration("ROOT", LogLevel.INFO, LogLevel.INFO), new LoggerConfiguration("com.example", LogLevel.DEBUG, LogLevel.DEBUG))); + given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); + given(this.loggingGroups.getLoggerGroupNames()).willReturn(Collections.singleton("test")); + given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member")); + given(this.loggingGroups.getLoggerGroupConfiguredLevel("test")).willReturn(LogLevel.INFO); this.mockMvc.perform(get("/actuator/loggers")).andExpect(status().isOk()) .andDo(MockMvcRestDocumentation.document("loggers/all", responseFields(fieldWithPath("levels").description("Levels support by the logging system."), - fieldWithPath("loggers").description("Loggers keyed by name.")) - .andWithPrefix("loggers.*.", levelFields))); + fieldWithPath("loggers").description("Loggers keyed by name."), + fieldWithPath("groups").description("Logger groups keyed by name")) + .andWithPrefix("loggers.*.", levelFields) + .andWithPrefix("groups.*.", groupLevelFields))); } @Test void logger() throws Exception { + given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); given(this.loggingSystem.getLoggerConfiguration("com.example")) .willReturn(new LoggerConfiguration("com.example", LogLevel.INFO, LogLevel.INFO)); this.mockMvc.perform(get("/actuator/loggers/com.example")).andExpect(status().isOk()) .andDo(MockMvcRestDocumentation.document("loggers/single", responseFields(levelFields))); } + @Test + void loggerGroups() throws Exception { + given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); + given(this.loggingGroups.isGroup("com.example")).willReturn(true); + given(this.loggingGroups.getLoggerGroup("com.example")).willReturn(Arrays.asList("com.member", "com.member2")); + given(this.loggingGroups.getLoggerGroupConfiguredLevel("com.example")).willReturn(LogLevel.INFO); + this.mockMvc.perform(get("/actuator/loggers/com.example")).andExpect(status().isOk()) + .andDo(MockMvcRestDocumentation.document("loggers/group", responseFields(groupLevelFields))); + } + @Test void setLogLevel() throws Exception { + given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); this.mockMvc .perform(post("/actuator/loggers/com.example").content("{\"configuredLevel\":\"debug\"}") .contentType(MediaType.APPLICATION_JSON)) @@ -89,8 +120,24 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest verify(this.loggingSystem).setLogLevel("com.example", LogLevel.DEBUG); } + @Test + void setLogLevelOfLoggerGroup() throws Exception { + given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); + given(this.loggingGroups.isGroup("com.example")).willReturn(true); + this.mockMvc + .perform(post("/actuator/loggers/com.example") + .content("{\"configuredLevel\":\"debug\"}").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()).andDo( + MockMvcRestDocumentation.document("loggers/setGroup", + requestFields(fieldWithPath("configuredLevel").description( + "Level for the logger group. May be omitted to clear the level of the loggers.") + .optional()))); + verify(this.loggingGroups).setLoggerGroupLevel("com.example", LogLevel.DEBUG); + } + @Test void clearLogLevel() throws Exception { + given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); this.mockMvc .perform(post("/actuator/loggers/com.example").content("{}").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()).andDo(MockMvcRestDocumentation.document("loggers/clear")); @@ -102,8 +149,9 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest static class TestConfiguration { @Bean - LoggersEndpoint endpoint(LoggingSystem loggingSystem) { - return new LoggersEndpoint(loggingSystem); + LoggersEndpoint endpoint(LoggingSystem loggingSystem, + ObjectProvider loggingGroupsObjectProvider) { + return new LoggersEndpoint(loggingSystem, loggingGroupsObjectProvider.getIfAvailable()); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java index ac575565768..25631d98ef6 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java @@ -19,6 +19,7 @@ package org.springframework.boot.actuate.logging; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.NavigableSet; import java.util.Set; @@ -30,6 +31,7 @@ import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; +import org.springframework.boot.logging.LoggingGroups; import org.springframework.boot.logging.LoggingSystem; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -39,6 +41,7 @@ import org.springframework.util.Assert; * * @author Ben Hale * @author Phillip Webb + * @author HaiTao Zhang * @since 2.0.0 */ @Endpoint(id = "loggers") @@ -46,13 +49,17 @@ public class LoggersEndpoint { private final LoggingSystem loggingSystem; + private final LoggingGroups loggingGroups; + /** * Create a new {@link LoggersEndpoint} instance. * @param loggingSystem the logging system to expose + * @param loggingGroups the logging group to expose if it exists */ - public LoggersEndpoint(LoggingSystem loggingSystem) { + public LoggersEndpoint(LoggingSystem loggingSystem, LoggingGroups loggingGroups) { Assert.notNull(loggingSystem, "LoggingSystem must not be null"); this.loggingSystem = loggingSystem; + this.loggingGroups = loggingGroups; } @ReadOperation @@ -64,19 +71,32 @@ public class LoggersEndpoint { Map result = new LinkedHashMap<>(); result.put("levels", getLevels()); result.put("loggers", getLoggers(configurations)); + if (this.loggingGroups != null && this.loggingGroups.getLoggerGroupNames() != null) { + Set groups = this.loggingGroups.getLoggerGroupNames(); + result.put("groups", getLoggerGroups(groups)); + } return result; } @ReadOperation public LoggerLevels loggerLevels(@Selector String name) { Assert.notNull(name, "Name must not be null"); + if (this.loggingGroups != null && this.loggingGroups.isGroup(name)) { + List members = this.loggingGroups.getLoggerGroup(name); + LogLevel groupConfiguredLevel = this.loggingGroups.getLoggerGroupConfiguredLevel(name); + return new GroupLoggerLevels(groupConfiguredLevel, members); + } LoggerConfiguration configuration = this.loggingSystem.getLoggerConfiguration(name); - return (configuration != null) ? new LoggerLevels(configuration) : null; + return (configuration != null) ? new SingleLoggerLevels(configuration) : null; } @WriteOperation public void configureLogLevel(@Selector String name, @Nullable LogLevel configuredLevel) { Assert.notNull(name, "Name must not be empty"); + if (this.loggingGroups != null && this.loggingGroups.isGroup(name)) { + this.loggingGroups.setLoggerGroupLevel(name, configuredLevel); + return; + } this.loggingSystem.setLogLevel(name, configuredLevel); } @@ -88,11 +108,21 @@ public class LoggersEndpoint { private Map getLoggers(Collection configurations) { Map loggers = new LinkedHashMap<>(configurations.size()); for (LoggerConfiguration configuration : configurations) { - loggers.put(configuration.getName(), new LoggerLevels(configuration)); + loggers.put(configuration.getName(), new SingleLoggerLevels(configuration)); } return loggers; } + private Map getLoggerGroups(Set groups) { + Map loggerGroups = new LinkedHashMap<>(groups.size()); + for (String name : groups) { + List members = this.loggingGroups.getLoggerGroup(name); + LogLevel groupConfiguredLevel = this.loggingGroups.getLoggerGroupConfiguredLevel(name); + loggerGroups.put(name, new GroupLoggerLevels(groupConfiguredLevel, members)); + } + return loggerGroups; + } + /** * Levels configured for a given logger exposed in a JSON friendly way. */ @@ -100,11 +130,8 @@ public class LoggersEndpoint { private String configuredLevel; - private String effectiveLevel; - - public LoggerLevels(LoggerConfiguration configuration) { - this.configuredLevel = getName(configuration.getConfiguredLevel()); - this.effectiveLevel = getName(configuration.getEffectiveLevel()); + public LoggerLevels(LogLevel configuredLevel) { + this.configuredLevel = getName(configuredLevel); } private String getName(LogLevel level) { @@ -113,6 +140,33 @@ public class LoggersEndpoint { public String getConfiguredLevel() { return this.configuredLevel; + + } + + } + + public static class GroupLoggerLevels extends LoggerLevels { + + private List members; + + public GroupLoggerLevels(LogLevel configuredLevel, List members) { + super(configuredLevel); + this.members = members; + } + + public List getMembers() { + return this.members; + } + + } + + public static class SingleLoggerLevels extends LoggerLevels { + + private String effectiveLevel; + + public SingleLoggerLevels(LoggerConfiguration configuration) { + super(configuration.getConfiguredLevel()); + this.effectiveLevel = super.getName(configuration.getEffectiveLevel()); } public String getEffectiveLevel() { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointTests.java index a83034508e9..29b766abd25 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointTests.java @@ -23,9 +23,12 @@ import java.util.Set; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.logging.LoggersEndpoint.GroupLoggerLevels; import org.springframework.boot.actuate.logging.LoggersEndpoint.LoggerLevels; +import org.springframework.boot.actuate.logging.LoggersEndpoint.SingleLoggerLevels; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; +import org.springframework.boot.logging.LoggingGroups; import org.springframework.boot.logging.LoggingSystem; import static org.assertj.core.api.Assertions.assertThat; @@ -38,46 +41,110 @@ import static org.mockito.Mockito.verify; * * @author Ben Hale * @author Andy Wilkinson + * @author HaiTao Zhang */ class LoggersEndpointTests { private final LoggingSystem loggingSystem = mock(LoggingSystem.class); + private final LoggingGroups loggingGroups = mock(LoggingGroups.class); + + @Test + @SuppressWarnings("unchecked") + void loggersShouldReturnLoggerConfigurationsWithNoLoggerGroups() { + given(this.loggingSystem.getLoggerConfigurations()) + .willReturn(Collections.singletonList(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG))); + given(this.loggingSystem.getSupportedLogLevels()).willReturn(EnumSet.allOf(LogLevel.class)); + given(this.loggingGroups.getLoggerGroupNames()).willReturn(null); + Map result = new LoggersEndpoint(this.loggingSystem, this.loggingGroups).loggers(); + Map loggers = (Map) result.get("loggers"); + Set levels = (Set) result.get("levels"); + SingleLoggerLevels rootLevels = (SingleLoggerLevels) loggers.get("ROOT"); + assertThat(rootLevels.getConfiguredLevel()).isNull(); + assertThat(rootLevels.getEffectiveLevel()).isEqualTo("DEBUG"); + assertThat(levels).containsExactly(LogLevel.OFF, LogLevel.FATAL, LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO, + LogLevel.DEBUG, LogLevel.TRACE); + assertThat(result.get("groups")).isNull(); + } + @Test @SuppressWarnings("unchecked") - void loggersShouldReturnLoggerConfigurations() { + void loggersShouldReturnLoggerConfigurationsWithLoggerGroups() { given(this.loggingSystem.getLoggerConfigurations()) .willReturn(Collections.singletonList(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG))); given(this.loggingSystem.getSupportedLogLevels()).willReturn(EnumSet.allOf(LogLevel.class)); - Map result = new LoggersEndpoint(this.loggingSystem).loggers(); + given(this.loggingGroups.getLoggerGroup("test")).willReturn(Collections.singletonList("test.member")); + given(this.loggingGroups.getLoggerGroupNames()).willReturn(Collections.singleton("test")); + given(this.loggingGroups.getLoggerGroupConfiguredLevel("test")).willReturn(LogLevel.DEBUG); + Map result = new LoggersEndpoint(this.loggingSystem, this.loggingGroups).loggers(); + Map loggerGroups = (Map) result.get("groups"); + GroupLoggerLevels testLoggerLevel = (GroupLoggerLevels) loggerGroups.get("test"); Map loggers = (Map) result.get("loggers"); Set levels = (Set) result.get("levels"); - LoggerLevels rootLevels = loggers.get("ROOT"); + SingleLoggerLevels rootLevels = (SingleLoggerLevels) loggers.get("ROOT"); assertThat(rootLevels.getConfiguredLevel()).isNull(); assertThat(rootLevels.getEffectiveLevel()).isEqualTo("DEBUG"); assertThat(levels).containsExactly(LogLevel.OFF, LogLevel.FATAL, LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO, LogLevel.DEBUG, LogLevel.TRACE); + assertThat(loggerGroups).isNotNull(); + assertThat(testLoggerLevel).isNotNull(); + assertThat(testLoggerLevel.getConfiguredLevel()).isEqualTo("DEBUG"); + assertThat(testLoggerLevel.getMembers()).isEqualTo(Collections.singletonList("test.member")); } @Test void loggerLevelsWhenNameSpecifiedShouldReturnLevels() { + given(this.loggingGroups.isGroup("ROOT")).willReturn(false); given(this.loggingSystem.getLoggerConfiguration("ROOT")) .willReturn(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG)); - LoggerLevels levels = new LoggersEndpoint(this.loggingSystem).loggerLevels("ROOT"); + SingleLoggerLevels levels = (SingleLoggerLevels) new LoggersEndpoint(this.loggingSystem, this.loggingGroups) + .loggerLevels("ROOT"); assertThat(levels.getConfiguredLevel()).isNull(); assertThat(levels.getEffectiveLevel()).isEqualTo("DEBUG"); } + @Test + void groupNameSpecifiedShouldReturnConfiguredLevelAndMembers() { + given(this.loggingGroups.isGroup("test")).willReturn(true); + given(this.loggingGroups.getLoggerGroup("test")).willReturn(Collections.singletonList("test.member")); + given(this.loggingGroups.getLoggerGroupConfiguredLevel("test")).willReturn(LogLevel.DEBUG); + GroupLoggerLevels levels = (GroupLoggerLevels) new LoggersEndpoint(this.loggingSystem, this.loggingGroups) + .loggerLevels("test"); + assertThat(levels.getConfiguredLevel()).isEqualTo("DEBUG"); + assertThat(levels.getMembers()).isEqualTo(Collections.singletonList("test.member")); + } + @Test void configureLogLevelShouldSetLevelOnLoggingSystem() { - new LoggersEndpoint(this.loggingSystem).configureLogLevel("ROOT", LogLevel.DEBUG); + given(this.loggingGroups.getLoggerGroup("ROOT")).willReturn(null); + new LoggersEndpoint(this.loggingSystem, this.loggingGroups).configureLogLevel("ROOT", LogLevel.DEBUG); verify(this.loggingSystem).setLogLevel("ROOT", LogLevel.DEBUG); } @Test void configureLogLevelWithNullSetsLevelOnLoggingSystemToNull() { - new LoggersEndpoint(this.loggingSystem).configureLogLevel("ROOT", null); + given(this.loggingGroups.getLoggerGroup("ROOT")).willReturn(null); + new LoggersEndpoint(this.loggingSystem, this.loggingGroups).configureLogLevel("ROOT", null); verify(this.loggingSystem).setLogLevel("ROOT", null); } + @Test + void configureLogLevelInLoggerGroupShouldSetLevelOnLoggingSystem() { + given(this.loggingGroups.isGroup("test")).willReturn(true); + given(this.loggingGroups.getLoggerGroup("test")).willReturn(Collections.singletonList("test.member")); + new LoggersEndpoint(this.loggingSystem, this.loggingGroups).configureLogLevel("test", LogLevel.DEBUG); + verify(this.loggingGroups).setLoggerGroupLevel("test", LogLevel.DEBUG); + } + + @Test + void configureLogLevelWithNullInLoggerGroupShouldSetLevelOnLoggingSystem() { + given(this.loggingGroups.isGroup("test")).willReturn(true); + given(this.loggingGroups.getLoggerGroup("test")).willReturn(Collections.singletonList("test.member")); + new LoggersEndpoint(this.loggingSystem, this.loggingGroups).configureLogLevel("test", null); + verify(this.loggingGroups).setLoggerGroupLevel("test", null); + } + + // @Test + // void + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointWebIntegrationTests.java index 97de0498354..9ce47c08141 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointWebIntegrationTests.java @@ -21,14 +21,17 @@ import java.util.Collections; import java.util.EnumSet; import net.minidev.json.JSONArray; +import org.hamcrest.collection.IsIterableContainingInAnyOrder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.mockito.Mockito; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; +import org.springframework.boot.logging.LoggingGroups; import org.springframework.boot.logging.LoggingSystem; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -50,6 +53,7 @@ import static org.mockito.Mockito.verifyZeroInteractions; * @author EddĂș MelĂ©ndez * @author Stephane Nicoll * @author Andy Wilkinson + * @author HaiTao Zhang */ class LoggersEndpointWebIntegrationTests { @@ -57,17 +61,26 @@ class LoggersEndpointWebIntegrationTests { private LoggingSystem loggingSystem; + private LoggingGroups loggingGroups; + + private ObjectProvider loggingGroupsObjectProvider; + @BeforeEach @AfterEach void resetMocks(ConfigurableApplicationContext context, WebTestClient client) { this.client = client; this.loggingSystem = context.getBean(LoggingSystem.class); + this.loggingGroups = context.getBean(LoggingGroups.class); + this.loggingGroupsObjectProvider = context.getBean(ObjectProvider.class); Mockito.reset(this.loggingSystem); + Mockito.reset(this.loggingGroups); given(this.loggingSystem.getSupportedLogLevels()).willReturn(EnumSet.allOf(LogLevel.class)); + given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); } @WebEndpointTest void getLoggerShouldReturnAllLoggerConfigurations() { + given(this.loggingGroups.getLoggerGroupNames()).willReturn(null); given(this.loggingSystem.getLoggerConfigurations()) .willReturn(Collections.singletonList(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG))); this.client.get().uri("/actuator/loggers").exchange().expectStatus().isOk().expectBody().jsonPath("$.length()") @@ -78,8 +91,27 @@ class LoggersEndpointWebIntegrationTests { .isEqualTo("DEBUG"); } + @WebEndpointTest + void getLoggerShouldReturnAllLoggerConfigurationsWithLoggerGroups() { + given(this.loggingGroups.getLoggerGroupNames()).willReturn(Collections.singleton("test")); + given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member1", "test.member2")); + given(this.loggingGroups.getLoggerGroupConfiguredLevel("test")).willReturn(LogLevel.DEBUG); + given(this.loggingSystem.getLoggerConfigurations()) + .willReturn(Collections.singletonList(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG))); + this.client.get().uri("/actuator/loggers").exchange().expectStatus().isOk().expectBody().jsonPath("$.length()") + .isEqualTo(3).jsonPath("levels") + .isEqualTo(jsonArrayOf("OFF", "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE")) + .jsonPath("loggers.length()").isEqualTo(1).jsonPath("loggers.ROOT.length()").isEqualTo(2) + .jsonPath("loggers.ROOT.configuredLevel").isEqualTo(null).jsonPath("loggers.ROOT.effectiveLevel") + .isEqualTo("DEBUG").jsonPath("groups.length()").isEqualTo(1).jsonPath("groups.test.length()") + .isEqualTo(2).jsonPath("groups.test.configuredLevel").isEqualTo("DEBUG") + .jsonPath("groups.test.members.length()").isEqualTo(2).jsonPath("groups.test.members") + .value(IsIterableContainingInAnyOrder.containsInAnyOrder("test.member1", "test.member2")); + } + @WebEndpointTest void getLoggerShouldReturnLogLevels() { + given(this.loggingGroups.isGroup("ROOT")).willReturn(false); given(this.loggingSystem.getLoggerConfiguration("ROOT")) .willReturn(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG)); this.client.get().uri("/actuator/loggers/ROOT").exchange().expectStatus().isOk().expectBody() @@ -88,12 +120,24 @@ class LoggersEndpointWebIntegrationTests { } @WebEndpointTest - void getLoggersWhenLoggerNotFoundShouldReturnNotFound() { + void getLoggersWhenLoggerAndLoggerGroupNotFoundShouldReturnNotFound() { this.client.get().uri("/actuator/loggers/com.does.not.exist").exchange().expectStatus().isNotFound(); } + @WebEndpointTest + void getLoggerGroupShouldReturnConfiguredLogLevelAndMembers() { + given(this.loggingGroups.isGroup("test")).willReturn(true); + given(this.loggingGroups.getLoggerGroupConfiguredLevel("test")).willReturn(LogLevel.DEBUG); + given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member1", "test.member2")); + this.client.get().uri("actuator/loggers/test").exchange().expectStatus().isOk().expectBody() + .jsonPath("$.length()").isEqualTo(2).jsonPath("members") + .value(IsIterableContainingInAnyOrder.containsInAnyOrder("test.member1", "test.member2")) + .jsonPath("configuredLevel").isEqualTo("DEBUG"); + } + @WebEndpointTest void setLoggerUsingApplicationJsonShouldSetLogLevel() { + given(this.loggingGroups.isGroup("ROOT")).willReturn(false); this.client.post().uri("/actuator/loggers/ROOT").contentType(MediaType.APPLICATION_JSON) .body(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus().isNoContent(); verify(this.loggingSystem).setLogLevel("ROOT", LogLevel.DEBUG); @@ -101,6 +145,7 @@ class LoggersEndpointWebIntegrationTests { @WebEndpointTest void setLoggerUsingActuatorV2JsonShouldSetLogLevel() { + given(this.loggingGroups.isGroup("ROOT")).willReturn(false); this.client.post().uri("/actuator/loggers/ROOT") .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)) .body(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus().isNoContent(); @@ -108,7 +153,26 @@ class LoggersEndpointWebIntegrationTests { } @WebEndpointTest - void setLoggerWithWrongLogLevelResultInBadRequestResponse() { + void setLoggerGroupUsingActuatorV2JsonShouldSetLogLevel() { + given(this.loggingGroups.isGroup("test")).willReturn(true); + given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member1", "test.member2")); + this.client.post().uri("/actuator/loggers/test") + .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)) + .body(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus().isNoContent(); + verify(this.loggingGroups).setLoggerGroupLevel("test", LogLevel.DEBUG); + } + + @WebEndpointTest + void setLoggerGroupUsingApplicationJsonShouldSetLogLevel() { + given(this.loggingGroups.isGroup("test")).willReturn(true); + given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member1", "test.member2")); + this.client.post().uri("/actuator/loggers/test").contentType(MediaType.APPLICATION_JSON) + .body(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus().isNoContent(); + verify(this.loggingGroups).setLoggerGroupLevel("test", LogLevel.DEBUG); + } + + @WebEndpointTest + void setLoggerOrLoggerGroupWithWrongLogLevelResultInBadRequestResponse() { this.client.post().uri("/actuator/loggers/ROOT").contentType(MediaType.APPLICATION_JSON) .body(Collections.singletonMap("configuredLevel", "other")).exchange().expectStatus().isBadRequest(); verifyZeroInteractions(this.loggingSystem); @@ -116,6 +180,7 @@ class LoggersEndpointWebIntegrationTests { @WebEndpointTest void setLoggerWithNullLogLevel() { + given(this.loggingGroups.isGroup("ROOT")).willReturn(false); this.client.post().uri("/actuator/loggers/ROOT") .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)) .body(Collections.singletonMap("configuredLevel", null)).exchange().expectStatus().isNoContent(); @@ -124,12 +189,33 @@ class LoggersEndpointWebIntegrationTests { @WebEndpointTest void setLoggerWithNoLogLevel() { + given(this.loggingGroups.isGroup("ROOT")).willReturn(false); this.client.post().uri("/actuator/loggers/ROOT") .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)).body(Collections.emptyMap()) .exchange().expectStatus().isNoContent(); verify(this.loggingSystem).setLogLevel("ROOT", null); } + @WebEndpointTest + void setLoggerGroupWithNullLogLevel() { + given(this.loggingGroups.isGroup("test")).willReturn(true); + given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member1", "test.member2")); + this.client.post().uri("/actuator/loggers/test") + .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)) + .body(Collections.singletonMap("configuredLevel", null)).exchange().expectStatus().isNoContent(); + verify(this.loggingGroups).setLoggerGroupLevel("test", null); + } + + @WebEndpointTest + void setLoggerGroupWithNoLogLevel() { + given(this.loggingGroups.isGroup("test")).willReturn(true); + given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member1", "test.member2")); + this.client.post().uri("/actuator/loggers/test") + .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)).body(Collections.emptyMap()) + .exchange().expectStatus().isNoContent(); + verify(this.loggingGroups).setLoggerGroupLevel("test", null); + } + @WebEndpointTest void logLevelForLoggerWithNameThatCouldBeMistakenForAPathExtension() { given(this.loggingSystem.getLoggerConfiguration("com.png")) @@ -139,6 +225,16 @@ class LoggersEndpointWebIntegrationTests { .jsonPath("effectiveLevel").isEqualTo("DEBUG"); } + @WebEndpointTest + void logLevelForLoggerGroupWithNameThatCouldBeMistakenForAPathExtension() { + given(this.loggingGroups.isGroup("com.png")).willReturn(true); + given(this.loggingGroups.getLoggerGroupConfiguredLevel("com.png")).willReturn(LogLevel.DEBUG); + given(this.loggingGroups.getLoggerGroup("com.png")).willReturn(Arrays.asList("test.member1", "test.member2")); + this.client.get().uri("/actuator/loggers/com.png").exchange().expectStatus().isOk().expectBody() + .jsonPath("$.length()").isEqualTo(2).jsonPath("configuredLevel").isEqualTo("DEBUG").jsonPath("members") + .value(IsIterableContainingInAnyOrder.containsInAnyOrder("test.member1", "test.member2")); + } + private JSONArray jsonArrayOf(Object... entries) { JSONArray array = new JSONArray(); array.addAll(Arrays.asList(entries)); @@ -154,8 +250,19 @@ class LoggersEndpointWebIntegrationTests { } @Bean - LoggersEndpoint endpoint(LoggingSystem loggingSystem) { - return new LoggersEndpoint(loggingSystem); + ObjectProvider loggingGroupsObjectProvider() { + return mock(ObjectProvider.class); + } + + @Bean + LoggingGroups loggingGroups() { + return mock(LoggingGroups.class); + } + + @Bean + LoggersEndpoint endpoint(LoggingSystem loggingSystem, + ObjectProvider loggingGroupsObjectProvider) { + return new LoggersEndpoint(loggingSystem, loggingGroupsObjectProvider.getIfAvailable()); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java index f69202c8331..9dab14b12d6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java @@ -16,6 +16,7 @@ package org.springframework.boot.context.logging; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -36,6 +37,7 @@ import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggingGroups; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystemProperties; @@ -126,6 +128,11 @@ public class LoggingApplicationListener implements GenericApplicationListener { */ public static final String LOGFILE_BEAN_NAME = "springBootLogFile"; + /** + * The name of the{@link LoggingGroups} bean. + */ + public static final String LOGGING_GROUPS_BEAN_NAME = "springBootLoggingGroups"; + private static final Map> DEFAULT_GROUP_LOGGERS; static { MultiValueMap loggers = new LinkedMultiValueMap<>(); @@ -166,6 +173,8 @@ public class LoggingApplicationListener implements GenericApplicationListener { private LoggingSystem loggingSystem; + private LoggingGroups loggingGroups; + private LogFile logFile; private int order = DEFAULT_ORDER; @@ -235,6 +244,9 @@ public class LoggingApplicationListener implements GenericApplicationListener { if (this.logFile != null && !beanFactory.containsBean(LOGFILE_BEAN_NAME)) { beanFactory.registerSingleton(LOGFILE_BEAN_NAME, this.logFile); } + if (this.loggingGroups != null && !beanFactory.containsBean(LOGGING_GROUPS_BEAN_NAME)) { + beanFactory.registerSingleton(LOGGING_GROUPS_BEAN_NAME, this.loggingGroups); + } } private void onContextClosedEvent() { @@ -257,6 +269,7 @@ public class LoggingApplicationListener implements GenericApplicationListener { */ protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) { new LoggingSystemProperties(environment).apply(); + this.loggingGroups = new LoggingGroups(this.loggingSystem); this.logFile = LogFile.get(environment); if (this.logFile != null) { this.logFile.applyToSystemProperties(); @@ -325,7 +338,8 @@ public class LoggingApplicationListener implements GenericApplicationListener { system.setLogLevel(logger, level); return; } - groupLoggers.forEach((groupLogger) -> system.setLogLevel(groupLogger, level)); + this.loggingGroups.setLoggerGroup(logger, groupLoggers); + this.loggingGroups.setLoggerGroupLevel(logger, level); } protected void setLogLevels(LoggingSystem system, Environment environment) { @@ -342,7 +356,7 @@ public class LoggingApplicationListener implements GenericApplicationListener { setLogLevel(system, name, level); } else { - setLogLevel(system, groupedNames, level); + setLogLevel(groupedNames, level, name); } }); } @@ -353,9 +367,13 @@ public class LoggingApplicationListener implements GenericApplicationListener { return groups; } - private void setLogLevel(LoggingSystem system, String[] names, LogLevel level) { - for (String name : names) { - setLogLevel(system, name, level); + private void setLogLevel(String[] names, LogLevel level, String groupName) { + try { + this.loggingGroups.setLoggerGroup(groupName, Arrays.asList(names)); + this.loggingGroups.setLoggerGroupLevel(groupName, level); + } + catch (RuntimeException ex) { + this.logger.error("Cannot set level '" + level + "' for '" + groupName + "'"); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingGroups.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingGroups.java new file mode 100644 index 00000000000..a6b1be08b3a --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingGroups.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.util.Assert; + +/** + * Manage logger groups. + * + * @author HaiTao Zhang + * @since 2.2.0 + */ +public class LoggingGroups { + + private Map loggerGroupConfigurations; + + private Map> loggerGroups; + + private LoggingSystem loggingSystem; + + public LoggingGroups(LoggingSystem loggingSystem) { + this.loggerGroupConfigurations = new ConcurrentHashMap<>(); + this.loggerGroups = new ConcurrentHashMap<>(); + this.loggingSystem = loggingSystem; + } + + /** + * Associate a name to a list of logger's name to create a logger group. + * @param groupName name of the logger group + * @param members list of the members names + */ + public void setLoggerGroup(String groupName, List members) { + Assert.notNull(groupName, "Group name can not be null"); + Assert.notNull(members, "Members can not be null"); + this.loggerGroups.put(groupName, members); + } + + /** + * Set the logging level for a given logger group. + * @param groupName the name of the group to set + * @param level the log level ({@code null}) can be used to remove any custom level + * for the logger group and use the default configuration instead. + */ + public void setLoggerGroupLevel(String groupName, LogLevel level) { + Assert.notNull(groupName, "Group name can not be null"); + List members = this.loggerGroups.get(groupName); + members.forEach((member) -> this.loggingSystem + .setLogLevel(member.equalsIgnoreCase(LoggingSystem.ROOT_LOGGER_NAME) ? null : member, level)); + this.loggerGroupConfigurations.put(groupName, level); + } + + /** + * Checks whether a groupName is associated to a logger group. + * @param groupName name of the logger group + * @return a boolean stating true when groupName is associated with a group of loggers + */ + public boolean isGroup(String groupName) { + Assert.notNull(groupName, "Group name can not be null"); + return this.loggerGroups.containsKey(groupName); + } + + /** + * Get the all registered logger groups. + * @return a Set of the names of the logger groups + */ + public Set getLoggerGroupNames() { + synchronized (this) { + return this.loggerGroups.isEmpty() ? null : Collections.unmodifiableSet(this.loggerGroups.keySet()); + } + } + + /** + * Get a logger group's members. + * @param groupName name of the logger group + * @return list of the members names associated with this group + */ + public List getLoggerGroup(String groupName) { + Assert.notNull(groupName, "Group name can not be null"); + return Collections.unmodifiableList(this.loggerGroups.get(groupName)); + } + + /** + * Get a logger group's configured level. + * @param groupName name of the logger group + * @return the logger groups configured level + */ + public LogLevel getLoggerGroupConfiguredLevel(String groupName) { + Assert.notNull(groupName, "Group name can not be null"); + return this.loggerGroupConfigurations.get(groupName); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingGroupsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingGroupsTests.java new file mode 100644 index 00000000000..b0ab8ff21c2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingGroupsTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link LoggingGroups} + * + * @author HaiTao Zhang + */ +public class LoggingGroupsTests { + + private LoggingSystem loggingSystem = mock(LoggingSystem.class); + + @Test + void setLoggerGroupWithTheConfiguredLevelToAllMembers() { + LoggingGroups loggingGroups = new LoggingGroups(this.loggingSystem); + loggingGroups.setLoggerGroup("test", Arrays.asList("test.member", "test.member2")); + loggingGroups.setLoggerGroupLevel("test", LogLevel.DEBUG); + verify(this.loggingSystem).setLogLevel("test.member2", LogLevel.DEBUG); + verify(this.loggingSystem).setLogLevel("test.member", LogLevel.DEBUG); + } + +} From 61b86ff2313993c2e234072de55bc2c2882e954c Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 30 Jul 2019 11:20:15 -0700 Subject: [PATCH 2/2] Polish "Add support for configuring logging groups" See gh-17515 Co-authored-by: Phillip Webb --- .../src/main/asciidoc/endpoints/loggers.adoc | 39 +++++ .../LoggersEndpointAutoConfiguration.java | 6 +- .../LoggersEndpointDocumentationTests.java | 59 +++---- .../boot/actuate/logging/LoggersEndpoint.java | 51 +++--- .../actuate/logging/LoggersEndpointTests.java | 64 ++++---- .../LoggersEndpointWebIntegrationTests.java | 95 +++++------- .../logging/LoggingApplicationListener.java | 146 +++++++++++------- .../boot/logging/LoggerGroup.java | 65 ++++++++ .../boot/logging/LoggerGroups.java | 63 ++++++++ .../boot/logging/LoggingGroups.java | 112 -------------- .../LoggingApplicationListenerTests.java | 3 + .../boot/logging/LoggerGroupsTests.java | 65 ++++++++ .../boot/logging/LoggingGroupsTests.java | 44 ------ 13 files changed, 442 insertions(+), 370 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggerGroup.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggerGroups.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingGroups.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggerGroupsTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingGroupsTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/loggers.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/loggers.adoc index ee7671faa22..e9a20b09e78 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/loggers.adoc +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/loggers.adoc @@ -57,6 +57,21 @@ include::{snippets}loggers/single/response-fields.adoc[] +[[loggers-group]] +== Retrieving a Single Group + +To retrieve a single group, make a `GET` request to `/actuator/loggers/{group.name}`, +as shown in the following curl-based example: + +include::{snippets}loggers/group/curl-request.adoc[] + +The preceding example retrieves information about the logger group named `test`. The +resulting response is similar to the following: + +include::{snippets}loggers/group/http-response.adoc[] + + + [[loggers-setting-level]] == Setting a Log Level @@ -81,6 +96,30 @@ include::{snippets}loggers/set/request-fields.adoc[] +[[loggers-setting-level]] +== Setting a Log Level for a Group + +To set the level of a logger, make a `POST` request to +`/actuator/loggers/{group.name}` with a JSON body that specifies the configured level +for the logger group, as shown in the following curl-based example: + +include::{snippets}loggers/setGroup/curl-request.adoc[] + +The preceding example sets the `configuredLevel` of the `test` logger group to `DEBUG`. + + + +[[loggers-setting-level-request-structure]] +=== Request Structure + +The request specifies the desired level of the logger group. The following table describes the +structure of the request: + +[cols="3,1,3"] +include::{snippets}loggers/set/request-fields.adoc[] + + + [[loggers-clearing-level]] == Clearing a Log Level diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/LoggersEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/LoggersEndpointAutoConfiguration.java index 96d419ae26c..244baa620dc 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/LoggersEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/LoggersEndpointAutoConfiguration.java @@ -25,7 +25,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.logging.LoggingGroups; +import org.springframework.boot.logging.LoggerGroups; import org.springframework.boot.logging.LoggingSystem; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; @@ -48,8 +48,8 @@ public class LoggersEndpointAutoConfiguration { @Conditional(OnEnabledLoggingSystemCondition.class) @ConditionalOnMissingBean public LoggersEndpoint loggersEndpoint(LoggingSystem loggingSystem, - ObjectProvider loggingGroupsObjectProvider) { - return new LoggersEndpoint(loggingSystem, loggingGroupsObjectProvider.getIfAvailable()); + ObjectProvider springBootLoggerGroups) { + return new LoggersEndpoint(loggingSystem, springBootLoggerGroups.getIfAvailable(LoggerGroups::new)); } static class OnEnabledLoggingSystemCondition extends SpringBootCondition { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LoggersEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LoggersEndpointDocumentationTests.java index 17ee607c844..37263c135ca 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LoggersEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LoggersEndpointDocumentationTests.java @@ -20,14 +20,15 @@ import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.logging.LoggersEndpoint; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; -import org.springframework.boot.logging.LoggingGroups; +import org.springframework.boot.logging.LoggerGroups; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; @@ -57,18 +58,20 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest fieldWithPath("configuredLevel").description("Configured level of the logger, if any.").optional(), fieldWithPath("effectiveLevel").description("Effective level of the logger.")); - private static final List groupLevelFields = Arrays.asList( - fieldWithPath("configuredLevel").description("Configured level of the logger group"), - fieldWithPath("members").description("Loggers that are part of this group").optional()); + private static final List groupLevelFields; - @MockBean - private LoggingSystem loggingSystem; + static { + groupLevelFields = Arrays.asList( + fieldWithPath("configuredLevel").description("Configured level of the logger group") + .type(LogLevel.class).optional(), + fieldWithPath("members").description("Loggers that are part of this group").optional()); + } @MockBean - private ObjectProvider loggingGroupsObjectProvider; + private LoggingSystem loggingSystem; - @MockBean - LoggingGroups loggingGroups; + @Autowired + private LoggerGroups loggerGroups; @Test void allLoggers() throws Exception { @@ -76,10 +79,6 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest given(this.loggingSystem.getLoggerConfigurations()) .willReturn(Arrays.asList(new LoggerConfiguration("ROOT", LogLevel.INFO, LogLevel.INFO), new LoggerConfiguration("com.example", LogLevel.DEBUG, LogLevel.DEBUG))); - given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); - given(this.loggingGroups.getLoggerGroupNames()).willReturn(Collections.singleton("test")); - given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member")); - given(this.loggingGroups.getLoggerGroupConfiguredLevel("test")).willReturn(LogLevel.INFO); this.mockMvc.perform(get("/actuator/loggers")).andExpect(status().isOk()) .andDo(MockMvcRestDocumentation.document("loggers/all", responseFields(fieldWithPath("levels").description("Levels support by the logging system."), @@ -91,7 +90,6 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest @Test void logger() throws Exception { - given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); given(this.loggingSystem.getLoggerConfiguration("com.example")) .willReturn(new LoggerConfiguration("com.example", LogLevel.INFO, LogLevel.INFO)); this.mockMvc.perform(get("/actuator/loggers/com.example")).andExpect(status().isOk()) @@ -100,17 +98,12 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest @Test void loggerGroups() throws Exception { - given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); - given(this.loggingGroups.isGroup("com.example")).willReturn(true); - given(this.loggingGroups.getLoggerGroup("com.example")).willReturn(Arrays.asList("com.member", "com.member2")); - given(this.loggingGroups.getLoggerGroupConfiguredLevel("com.example")).willReturn(LogLevel.INFO); - this.mockMvc.perform(get("/actuator/loggers/com.example")).andExpect(status().isOk()) + this.mockMvc.perform(get("/actuator/loggers/test")).andExpect(status().isOk()) .andDo(MockMvcRestDocumentation.document("loggers/group", responseFields(groupLevelFields))); } @Test void setLogLevel() throws Exception { - given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); this.mockMvc .perform(post("/actuator/loggers/com.example").content("{\"configuredLevel\":\"debug\"}") .contentType(MediaType.APPLICATION_JSON)) @@ -122,22 +115,26 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest @Test void setLogLevelOfLoggerGroup() throws Exception { - given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); - given(this.loggingGroups.isGroup("com.example")).willReturn(true); this.mockMvc - .perform(post("/actuator/loggers/com.example") + .perform(post("/actuator/loggers/test") .content("{\"configuredLevel\":\"debug\"}").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()).andDo( MockMvcRestDocumentation.document("loggers/setGroup", requestFields(fieldWithPath("configuredLevel").description( "Level for the logger group. May be omitted to clear the level of the loggers.") .optional()))); - verify(this.loggingGroups).setLoggerGroupLevel("com.example", LogLevel.DEBUG); + verify(this.loggingSystem).setLogLevel("test.member1", LogLevel.DEBUG); + verify(this.loggingSystem).setLogLevel("test.member2", LogLevel.DEBUG); + resetLogger(); + } + + private void resetLogger() { + this.loggerGroups.get("test").configureLogLevel(null, (a, b) -> { + }); } @Test void clearLogLevel() throws Exception { - given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); this.mockMvc .perform(post("/actuator/loggers/com.example").content("{}").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()).andDo(MockMvcRestDocumentation.document("loggers/clear")); @@ -149,9 +146,13 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest static class TestConfiguration { @Bean - LoggersEndpoint endpoint(LoggingSystem loggingSystem, - ObjectProvider loggingGroupsObjectProvider) { - return new LoggersEndpoint(loggingSystem, loggingGroupsObjectProvider.getIfAvailable()); + LoggersEndpoint endpoint(LoggingSystem loggingSystem, LoggerGroups groups) { + groups.putAll(getLoggerGroups()); + return new LoggersEndpoint(loggingSystem, groups); + } + + private Map> getLoggerGroups() { + return Collections.singletonMap("test", Arrays.asList("test.member1", "test.member2")); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java index 25631d98ef6..565fa076c5d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java @@ -31,7 +31,8 @@ import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; -import org.springframework.boot.logging.LoggingGroups; +import org.springframework.boot.logging.LoggerGroup; +import org.springframework.boot.logging.LoggerGroups; import org.springframework.boot.logging.LoggingSystem; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -49,17 +50,18 @@ public class LoggersEndpoint { private final LoggingSystem loggingSystem; - private final LoggingGroups loggingGroups; + private final LoggerGroups loggerGroups; /** * Create a new {@link LoggersEndpoint} instance. * @param loggingSystem the logging system to expose - * @param loggingGroups the logging group to expose if it exists + * @param loggerGroups the logger group to expose */ - public LoggersEndpoint(LoggingSystem loggingSystem, LoggingGroups loggingGroups) { + public LoggersEndpoint(LoggingSystem loggingSystem, LoggerGroups loggerGroups) { Assert.notNull(loggingSystem, "LoggingSystem must not be null"); + Assert.notNull(loggerGroups, "LoggerGroups must not be null"); this.loggingSystem = loggingSystem; - this.loggingGroups = loggingGroups; + this.loggerGroups = loggerGroups; } @ReadOperation @@ -71,20 +73,23 @@ public class LoggersEndpoint { Map result = new LinkedHashMap<>(); result.put("levels", getLevels()); result.put("loggers", getLoggers(configurations)); - if (this.loggingGroups != null && this.loggingGroups.getLoggerGroupNames() != null) { - Set groups = this.loggingGroups.getLoggerGroupNames(); - result.put("groups", getLoggerGroups(groups)); - } + result.put("groups", getGroups()); return result; } + private Map getGroups() { + Map groups = new LinkedHashMap<>(); + this.loggerGroups.forEach((group) -> groups.put(group.getName(), + new GroupLoggerLevels(group.getConfiguredLevel(), group.getMembers()))); + return groups; + } + @ReadOperation public LoggerLevels loggerLevels(@Selector String name) { Assert.notNull(name, "Name must not be null"); - if (this.loggingGroups != null && this.loggingGroups.isGroup(name)) { - List members = this.loggingGroups.getLoggerGroup(name); - LogLevel groupConfiguredLevel = this.loggingGroups.getLoggerGroupConfiguredLevel(name); - return new GroupLoggerLevels(groupConfiguredLevel, members); + LoggerGroup group = this.loggerGroups.get(name); + if (group != null) { + return new GroupLoggerLevels(group.getConfiguredLevel(), group.getMembers()); } LoggerConfiguration configuration = this.loggingSystem.getLoggerConfiguration(name); return (configuration != null) ? new SingleLoggerLevels(configuration) : null; @@ -93,8 +98,9 @@ public class LoggersEndpoint { @WriteOperation public void configureLogLevel(@Selector String name, @Nullable LogLevel configuredLevel) { Assert.notNull(name, "Name must not be empty"); - if (this.loggingGroups != null && this.loggingGroups.isGroup(name)) { - this.loggingGroups.setLoggerGroupLevel(name, configuredLevel); + LoggerGroup group = this.loggerGroups.get(name); + if (group != null && group.hasMembers()) { + group.configureLogLevel(configuredLevel, this.loggingSystem::setLogLevel); return; } this.loggingSystem.setLogLevel(name, configuredLevel); @@ -113,16 +119,6 @@ public class LoggersEndpoint { return loggers; } - private Map getLoggerGroups(Set groups) { - Map loggerGroups = new LinkedHashMap<>(groups.size()); - for (String name : groups) { - List members = this.loggingGroups.getLoggerGroup(name); - LogLevel groupConfiguredLevel = this.loggingGroups.getLoggerGroupConfiguredLevel(name); - loggerGroups.put(name, new GroupLoggerLevels(groupConfiguredLevel, members)); - } - return loggerGroups; - } - /** * Levels configured for a given logger exposed in a JSON friendly way. */ @@ -134,13 +130,12 @@ public class LoggersEndpoint { this.configuredLevel = getName(configuredLevel); } - private String getName(LogLevel level) { + protected final String getName(LogLevel level) { return (level != null) ? level.name() : null; } public String getConfiguredLevel() { return this.configuredLevel; - } } @@ -166,7 +161,7 @@ public class LoggersEndpoint { public SingleLoggerLevels(LoggerConfiguration configuration) { super(configuration.getConfiguredLevel()); - this.effectiveLevel = super.getName(configuration.getEffectiveLevel()); + this.effectiveLevel = getName(configuration.getEffectiveLevel()); } public String getEffectiveLevel() { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointTests.java index 29b766abd25..7ced35e133d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointTests.java @@ -18,9 +18,11 @@ package org.springframework.boot.actuate.logging; import java.util.Collections; import java.util.EnumSet; +import java.util.List; import java.util.Map; import java.util.Set; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.logging.LoggersEndpoint.GroupLoggerLevels; @@ -28,7 +30,7 @@ import org.springframework.boot.actuate.logging.LoggersEndpoint.LoggerLevels; import org.springframework.boot.actuate.logging.LoggersEndpoint.SingleLoggerLevels; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; -import org.springframework.boot.logging.LoggingGroups; +import org.springframework.boot.logging.LoggerGroups; import org.springframework.boot.logging.LoggingSystem; import static org.assertj.core.api.Assertions.assertThat; @@ -42,12 +44,21 @@ import static org.mockito.Mockito.verify; * @author Ben Hale * @author Andy Wilkinson * @author HaiTao Zhang + * @author Madhura Bhave */ class LoggersEndpointTests { private final LoggingSystem loggingSystem = mock(LoggingSystem.class); - private final LoggingGroups loggingGroups = mock(LoggingGroups.class); + private LoggerGroups loggerGroups; + + @BeforeEach + void setup() { + Map> groups = Collections.singletonMap("test", Collections.singletonList("test.member")); + this.loggerGroups = new LoggerGroups(groups); + this.loggerGroups.get("test").configureLogLevel(LogLevel.DEBUG, (a, b) -> { + }); + } @Test @SuppressWarnings("unchecked") @@ -55,8 +66,7 @@ class LoggersEndpointTests { given(this.loggingSystem.getLoggerConfigurations()) .willReturn(Collections.singletonList(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG))); given(this.loggingSystem.getSupportedLogLevels()).willReturn(EnumSet.allOf(LogLevel.class)); - given(this.loggingGroups.getLoggerGroupNames()).willReturn(null); - Map result = new LoggersEndpoint(this.loggingSystem, this.loggingGroups).loggers(); + Map result = new LoggersEndpoint(this.loggingSystem, new LoggerGroups()).loggers(); Map loggers = (Map) result.get("loggers"); Set levels = (Set) result.get("levels"); SingleLoggerLevels rootLevels = (SingleLoggerLevels) loggers.get("ROOT"); @@ -64,7 +74,8 @@ class LoggersEndpointTests { assertThat(rootLevels.getEffectiveLevel()).isEqualTo("DEBUG"); assertThat(levels).containsExactly(LogLevel.OFF, LogLevel.FATAL, LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO, LogLevel.DEBUG, LogLevel.TRACE); - assertThat(result.get("groups")).isNull(); + Map groups = (Map) result.get("groups"); + assertThat(groups).isEmpty(); } @Test @@ -73,12 +84,9 @@ class LoggersEndpointTests { given(this.loggingSystem.getLoggerConfigurations()) .willReturn(Collections.singletonList(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG))); given(this.loggingSystem.getSupportedLogLevels()).willReturn(EnumSet.allOf(LogLevel.class)); - given(this.loggingGroups.getLoggerGroup("test")).willReturn(Collections.singletonList("test.member")); - given(this.loggingGroups.getLoggerGroupNames()).willReturn(Collections.singleton("test")); - given(this.loggingGroups.getLoggerGroupConfiguredLevel("test")).willReturn(LogLevel.DEBUG); - Map result = new LoggersEndpoint(this.loggingSystem, this.loggingGroups).loggers(); - Map loggerGroups = (Map) result.get("groups"); - GroupLoggerLevels testLoggerLevel = (GroupLoggerLevels) loggerGroups.get("test"); + Map result = new LoggersEndpoint(this.loggingSystem, this.loggerGroups).loggers(); + Map loggerGroups = (Map) result.get("groups"); + GroupLoggerLevels groupLevel = loggerGroups.get("test"); Map loggers = (Map) result.get("loggers"); Set levels = (Set) result.get("levels"); SingleLoggerLevels rootLevels = (SingleLoggerLevels) loggers.get("ROOT"); @@ -87,17 +95,15 @@ class LoggersEndpointTests { assertThat(levels).containsExactly(LogLevel.OFF, LogLevel.FATAL, LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO, LogLevel.DEBUG, LogLevel.TRACE); assertThat(loggerGroups).isNotNull(); - assertThat(testLoggerLevel).isNotNull(); - assertThat(testLoggerLevel.getConfiguredLevel()).isEqualTo("DEBUG"); - assertThat(testLoggerLevel.getMembers()).isEqualTo(Collections.singletonList("test.member")); + assertThat(groupLevel.getConfiguredLevel()).isEqualTo("DEBUG"); + assertThat(groupLevel.getMembers()).containsExactly("test.member"); } @Test void loggerLevelsWhenNameSpecifiedShouldReturnLevels() { - given(this.loggingGroups.isGroup("ROOT")).willReturn(false); given(this.loggingSystem.getLoggerConfiguration("ROOT")) .willReturn(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG)); - SingleLoggerLevels levels = (SingleLoggerLevels) new LoggersEndpoint(this.loggingSystem, this.loggingGroups) + SingleLoggerLevels levels = (SingleLoggerLevels) new LoggersEndpoint(this.loggingSystem, this.loggerGroups) .loggerLevels("ROOT"); assertThat(levels.getConfiguredLevel()).isNull(); assertThat(levels.getEffectiveLevel()).isEqualTo("DEBUG"); @@ -105,10 +111,7 @@ class LoggersEndpointTests { @Test void groupNameSpecifiedShouldReturnConfiguredLevelAndMembers() { - given(this.loggingGroups.isGroup("test")).willReturn(true); - given(this.loggingGroups.getLoggerGroup("test")).willReturn(Collections.singletonList("test.member")); - given(this.loggingGroups.getLoggerGroupConfiguredLevel("test")).willReturn(LogLevel.DEBUG); - GroupLoggerLevels levels = (GroupLoggerLevels) new LoggersEndpoint(this.loggingSystem, this.loggingGroups) + GroupLoggerLevels levels = (GroupLoggerLevels) new LoggersEndpoint(this.loggingSystem, this.loggerGroups) .loggerLevels("test"); assertThat(levels.getConfiguredLevel()).isEqualTo("DEBUG"); assertThat(levels.getMembers()).isEqualTo(Collections.singletonList("test.member")); @@ -116,35 +119,26 @@ class LoggersEndpointTests { @Test void configureLogLevelShouldSetLevelOnLoggingSystem() { - given(this.loggingGroups.getLoggerGroup("ROOT")).willReturn(null); - new LoggersEndpoint(this.loggingSystem, this.loggingGroups).configureLogLevel("ROOT", LogLevel.DEBUG); + new LoggersEndpoint(this.loggingSystem, this.loggerGroups).configureLogLevel("ROOT", LogLevel.DEBUG); verify(this.loggingSystem).setLogLevel("ROOT", LogLevel.DEBUG); } @Test void configureLogLevelWithNullSetsLevelOnLoggingSystemToNull() { - given(this.loggingGroups.getLoggerGroup("ROOT")).willReturn(null); - new LoggersEndpoint(this.loggingSystem, this.loggingGroups).configureLogLevel("ROOT", null); + new LoggersEndpoint(this.loggingSystem, this.loggerGroups).configureLogLevel("ROOT", null); verify(this.loggingSystem).setLogLevel("ROOT", null); } @Test void configureLogLevelInLoggerGroupShouldSetLevelOnLoggingSystem() { - given(this.loggingGroups.isGroup("test")).willReturn(true); - given(this.loggingGroups.getLoggerGroup("test")).willReturn(Collections.singletonList("test.member")); - new LoggersEndpoint(this.loggingSystem, this.loggingGroups).configureLogLevel("test", LogLevel.DEBUG); - verify(this.loggingGroups).setLoggerGroupLevel("test", LogLevel.DEBUG); + new LoggersEndpoint(this.loggingSystem, this.loggerGroups).configureLogLevel("test", LogLevel.DEBUG); + verify(this.loggingSystem).setLogLevel("test.member", LogLevel.DEBUG); } @Test void configureLogLevelWithNullInLoggerGroupShouldSetLevelOnLoggingSystem() { - given(this.loggingGroups.isGroup("test")).willReturn(true); - given(this.loggingGroups.getLoggerGroup("test")).willReturn(Collections.singletonList("test.member")); - new LoggersEndpoint(this.loggingSystem, this.loggingGroups).configureLogLevel("test", null); - verify(this.loggingGroups).setLoggerGroupLevel("test", null); + new LoggersEndpoint(this.loggingSystem, this.loggerGroups).configureLogLevel("test", null); + verify(this.loggingSystem).setLogLevel("test.member", null); } - // @Test - // void - } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointWebIntegrationTests.java index 9ce47c08141..a3b857c6d3b 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointWebIntegrationTests.java @@ -19,6 +19,9 @@ package org.springframework.boot.actuate.logging; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import net.minidev.json.JSONArray; import org.hamcrest.collection.IsIterableContainingInAnyOrder; @@ -31,7 +34,7 @@ import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; -import org.springframework.boot.logging.LoggingGroups; +import org.springframework.boot.logging.LoggerGroups; import org.springframework.boot.logging.LoggingSystem; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -54,6 +57,7 @@ import static org.mockito.Mockito.verifyZeroInteractions; * @author Stephane Nicoll * @author Andy Wilkinson * @author HaiTao Zhang + * @author Madhura Bhave */ class LoggersEndpointWebIntegrationTests { @@ -61,41 +65,21 @@ class LoggersEndpointWebIntegrationTests { private LoggingSystem loggingSystem; - private LoggingGroups loggingGroups; - - private ObjectProvider loggingGroupsObjectProvider; + private LoggerGroups loggerGroups; @BeforeEach @AfterEach void resetMocks(ConfigurableApplicationContext context, WebTestClient client) { this.client = client; this.loggingSystem = context.getBean(LoggingSystem.class); - this.loggingGroups = context.getBean(LoggingGroups.class); - this.loggingGroupsObjectProvider = context.getBean(ObjectProvider.class); + this.loggerGroups = context.getBean(LoggerGroups.class); Mockito.reset(this.loggingSystem); - Mockito.reset(this.loggingGroups); given(this.loggingSystem.getSupportedLogLevels()).willReturn(EnumSet.allOf(LogLevel.class)); - given(this.loggingGroupsObjectProvider.getIfAvailable()).willReturn(this.loggingGroups); - } - - @WebEndpointTest - void getLoggerShouldReturnAllLoggerConfigurations() { - given(this.loggingGroups.getLoggerGroupNames()).willReturn(null); - given(this.loggingSystem.getLoggerConfigurations()) - .willReturn(Collections.singletonList(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG))); - this.client.get().uri("/actuator/loggers").exchange().expectStatus().isOk().expectBody().jsonPath("$.length()") - .isEqualTo(2).jsonPath("levels") - .isEqualTo(jsonArrayOf("OFF", "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE")) - .jsonPath("loggers.length()").isEqualTo(1).jsonPath("loggers.ROOT.length()").isEqualTo(2) - .jsonPath("loggers.ROOT.configuredLevel").isEqualTo(null).jsonPath("loggers.ROOT.effectiveLevel") - .isEqualTo("DEBUG"); } @WebEndpointTest void getLoggerShouldReturnAllLoggerConfigurationsWithLoggerGroups() { - given(this.loggingGroups.getLoggerGroupNames()).willReturn(Collections.singleton("test")); - given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member1", "test.member2")); - given(this.loggingGroups.getLoggerGroupConfiguredLevel("test")).willReturn(LogLevel.DEBUG); + setLogLevelToDebug("test"); given(this.loggingSystem.getLoggerConfigurations()) .willReturn(Collections.singletonList(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG))); this.client.get().uri("/actuator/loggers").exchange().expectStatus().isOk().expectBody().jsonPath("$.length()") @@ -103,15 +87,13 @@ class LoggersEndpointWebIntegrationTests { .isEqualTo(jsonArrayOf("OFF", "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE")) .jsonPath("loggers.length()").isEqualTo(1).jsonPath("loggers.ROOT.length()").isEqualTo(2) .jsonPath("loggers.ROOT.configuredLevel").isEqualTo(null).jsonPath("loggers.ROOT.effectiveLevel") - .isEqualTo("DEBUG").jsonPath("groups.length()").isEqualTo(1).jsonPath("groups.test.length()") - .isEqualTo(2).jsonPath("groups.test.configuredLevel").isEqualTo("DEBUG") - .jsonPath("groups.test.members.length()").isEqualTo(2).jsonPath("groups.test.members") - .value(IsIterableContainingInAnyOrder.containsInAnyOrder("test.member1", "test.member2")); + .isEqualTo("DEBUG").jsonPath("groups.length()").isEqualTo(2).jsonPath("groups.test.configuredLevel") + .isEqualTo("DEBUG"); } @WebEndpointTest void getLoggerShouldReturnLogLevels() { - given(this.loggingGroups.isGroup("ROOT")).willReturn(false); + setLogLevelToDebug("test"); given(this.loggingSystem.getLoggerConfiguration("ROOT")) .willReturn(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG)); this.client.get().uri("/actuator/loggers/ROOT").exchange().expectStatus().isOk().expectBody() @@ -126,9 +108,7 @@ class LoggersEndpointWebIntegrationTests { @WebEndpointTest void getLoggerGroupShouldReturnConfiguredLogLevelAndMembers() { - given(this.loggingGroups.isGroup("test")).willReturn(true); - given(this.loggingGroups.getLoggerGroupConfiguredLevel("test")).willReturn(LogLevel.DEBUG); - given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member1", "test.member2")); + setLogLevelToDebug("test"); this.client.get().uri("actuator/loggers/test").exchange().expectStatus().isOk().expectBody() .jsonPath("$.length()").isEqualTo(2).jsonPath("members") .value(IsIterableContainingInAnyOrder.containsInAnyOrder("test.member1", "test.member2")) @@ -137,7 +117,6 @@ class LoggersEndpointWebIntegrationTests { @WebEndpointTest void setLoggerUsingApplicationJsonShouldSetLogLevel() { - given(this.loggingGroups.isGroup("ROOT")).willReturn(false); this.client.post().uri("/actuator/loggers/ROOT").contentType(MediaType.APPLICATION_JSON) .body(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus().isNoContent(); verify(this.loggingSystem).setLogLevel("ROOT", LogLevel.DEBUG); @@ -145,7 +124,6 @@ class LoggersEndpointWebIntegrationTests { @WebEndpointTest void setLoggerUsingActuatorV2JsonShouldSetLogLevel() { - given(this.loggingGroups.isGroup("ROOT")).willReturn(false); this.client.post().uri("/actuator/loggers/ROOT") .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)) .body(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus().isNoContent(); @@ -154,21 +132,19 @@ class LoggersEndpointWebIntegrationTests { @WebEndpointTest void setLoggerGroupUsingActuatorV2JsonShouldSetLogLevel() { - given(this.loggingGroups.isGroup("test")).willReturn(true); - given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member1", "test.member2")); this.client.post().uri("/actuator/loggers/test") .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)) .body(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus().isNoContent(); - verify(this.loggingGroups).setLoggerGroupLevel("test", LogLevel.DEBUG); + verify(this.loggingSystem).setLogLevel("test.member1", LogLevel.DEBUG); + verify(this.loggingSystem).setLogLevel("test.member2", LogLevel.DEBUG); } @WebEndpointTest void setLoggerGroupUsingApplicationJsonShouldSetLogLevel() { - given(this.loggingGroups.isGroup("test")).willReturn(true); - given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member1", "test.member2")); this.client.post().uri("/actuator/loggers/test").contentType(MediaType.APPLICATION_JSON) .body(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus().isNoContent(); - verify(this.loggingGroups).setLoggerGroupLevel("test", LogLevel.DEBUG); + verify(this.loggingSystem).setLogLevel("test.member1", LogLevel.DEBUG); + verify(this.loggingSystem).setLogLevel("test.member2", LogLevel.DEBUG); } @WebEndpointTest @@ -180,7 +156,6 @@ class LoggersEndpointWebIntegrationTests { @WebEndpointTest void setLoggerWithNullLogLevel() { - given(this.loggingGroups.isGroup("ROOT")).willReturn(false); this.client.post().uri("/actuator/loggers/ROOT") .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)) .body(Collections.singletonMap("configuredLevel", null)).exchange().expectStatus().isNoContent(); @@ -189,7 +164,6 @@ class LoggersEndpointWebIntegrationTests { @WebEndpointTest void setLoggerWithNoLogLevel() { - given(this.loggingGroups.isGroup("ROOT")).willReturn(false); this.client.post().uri("/actuator/loggers/ROOT") .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)).body(Collections.emptyMap()) .exchange().expectStatus().isNoContent(); @@ -198,22 +172,20 @@ class LoggersEndpointWebIntegrationTests { @WebEndpointTest void setLoggerGroupWithNullLogLevel() { - given(this.loggingGroups.isGroup("test")).willReturn(true); - given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member1", "test.member2")); this.client.post().uri("/actuator/loggers/test") .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)) .body(Collections.singletonMap("configuredLevel", null)).exchange().expectStatus().isNoContent(); - verify(this.loggingGroups).setLoggerGroupLevel("test", null); + verify(this.loggingSystem).setLogLevel("test.member1", null); + verify(this.loggingSystem).setLogLevel("test.member2", null); } @WebEndpointTest void setLoggerGroupWithNoLogLevel() { - given(this.loggingGroups.isGroup("test")).willReturn(true); - given(this.loggingGroups.getLoggerGroup("test")).willReturn(Arrays.asList("test.member1", "test.member2")); this.client.post().uri("/actuator/loggers/test") .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)).body(Collections.emptyMap()) .exchange().expectStatus().isNoContent(); - verify(this.loggingGroups).setLoggerGroupLevel("test", null); + verify(this.loggingSystem).setLogLevel("test.member1", null); + verify(this.loggingSystem).setLogLevel("test.member2", null); } @WebEndpointTest @@ -227,12 +199,15 @@ class LoggersEndpointWebIntegrationTests { @WebEndpointTest void logLevelForLoggerGroupWithNameThatCouldBeMistakenForAPathExtension() { - given(this.loggingGroups.isGroup("com.png")).willReturn(true); - given(this.loggingGroups.getLoggerGroupConfiguredLevel("com.png")).willReturn(LogLevel.DEBUG); - given(this.loggingGroups.getLoggerGroup("com.png")).willReturn(Arrays.asList("test.member1", "test.member2")); - this.client.get().uri("/actuator/loggers/com.png").exchange().expectStatus().isOk().expectBody() + setLogLevelToDebug("group.png"); + this.client.get().uri("/actuator/loggers/group.png").exchange().expectStatus().isOk().expectBody() .jsonPath("$.length()").isEqualTo(2).jsonPath("configuredLevel").isEqualTo("DEBUG").jsonPath("members") - .value(IsIterableContainingInAnyOrder.containsInAnyOrder("test.member1", "test.member2")); + .value(IsIterableContainingInAnyOrder.containsInAnyOrder("png.member1", "png.member2")); + } + + private void setLogLevelToDebug(String name) { + this.loggerGroups.get(name).configureLogLevel(LogLevel.DEBUG, (a, b) -> { + }); } private JSONArray jsonArrayOf(Object... entries) { @@ -250,18 +225,20 @@ class LoggersEndpointWebIntegrationTests { } @Bean - ObjectProvider loggingGroupsObjectProvider() { - return mock(ObjectProvider.class); + LoggerGroups loggingGroups() { + return getLoggerGroups(); } - @Bean - LoggingGroups loggingGroups() { - return mock(LoggingGroups.class); + private LoggerGroups getLoggerGroups() { + Map> groups = new LinkedHashMap<>(); + groups.put("test", Arrays.asList("test.member1", "test.member2")); + groups.put("group.png", Arrays.asList("png.member1", "png.member2")); + return new LoggerGroups(groups); } @Bean LoggersEndpoint endpoint(LoggingSystem loggingSystem, - ObjectProvider loggingGroupsObjectProvider) { + ObjectProvider loggingGroupsObjectProvider) { return new LoggersEndpoint(loggingSystem, loggingGroupsObjectProvider.getIfAvailable()); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java index 9dab14b12d6..9742c9c90af 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java @@ -16,12 +16,11 @@ package org.springframework.boot.context.logging; -import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -37,7 +36,8 @@ import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LogLevel; -import org.springframework.boot.logging.LoggingGroups; +import org.springframework.boot.logging.LoggerGroup; +import org.springframework.boot.logging.LoggerGroups; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystemProperties; @@ -52,7 +52,6 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.util.ObjectUtils; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; @@ -85,6 +84,7 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Andy Wilkinson * @author Madhura Bhave + * @author HaiTao Zhang * @since 2.0.0 * @see LoggingSystem#get(ClassLoader) */ @@ -97,8 +97,8 @@ public class LoggingApplicationListener implements GenericApplicationListener { private static final Bindable> STRING_LOGLEVEL_MAP = Bindable.mapOf(String.class, LogLevel.class); - private static final Bindable> STRING_STRINGS_MAP = Bindable.mapOf(String.class, - String[].class); + private static final Bindable>> STRING_STRINGS_MAP = Bindable + .of(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class).asMap()); /** * The default order for the LoggingApplicationListener. @@ -129,9 +129,10 @@ public class LoggingApplicationListener implements GenericApplicationListener { public static final String LOGFILE_BEAN_NAME = "springBootLogFile"; /** - * The name of the{@link LoggingGroups} bean. + * The name of the{@link LoggerGroups} bean. + * @since 2.2.0 */ - public static final String LOGGING_GROUPS_BEAN_NAME = "springBootLoggingGroups"; + public static final String LOGGER_GROUPS_BEAN_NAME = "springBootLoggerGroups"; private static final Map> DEFAULT_GROUP_LOGGERS; static { @@ -147,7 +148,7 @@ public class LoggingApplicationListener implements GenericApplicationListener { DEFAULT_GROUP_LOGGERS = Collections.unmodifiableMap(loggers); } - private static final Map> LOG_LEVEL_LOGGERS; + private static final Map> SPRING_BOOT_LOGGING_LOGGERS; static { MultiValueMap loggers = new LinkedMultiValueMap<>(); loggers.add(LogLevel.DEBUG, "sql"); @@ -158,7 +159,7 @@ public class LoggingApplicationListener implements GenericApplicationListener { loggers.add(LogLevel.TRACE, "org.apache.catalina"); loggers.add(LogLevel.TRACE, "org.eclipse.jetty"); loggers.add(LogLevel.TRACE, "org.hibernate.tool.hbm2ddl"); - LOG_LEVEL_LOGGERS = Collections.unmodifiableMap(loggers); + SPRING_BOOT_LOGGING_LOGGERS = Collections.unmodifiableMap(loggers); } private static final Class[] EVENT_TYPES = { ApplicationStartingEvent.class, @@ -173,10 +174,10 @@ public class LoggingApplicationListener implements GenericApplicationListener { private LoggingSystem loggingSystem; - private LoggingGroups loggingGroups; - private LogFile logFile; + private LoggerGroups loggerGroups; + private int order = DEFAULT_ORDER; private boolean parseArgs = true; @@ -244,8 +245,8 @@ public class LoggingApplicationListener implements GenericApplicationListener { if (this.logFile != null && !beanFactory.containsBean(LOGFILE_BEAN_NAME)) { beanFactory.registerSingleton(LOGFILE_BEAN_NAME, this.logFile); } - if (this.loggingGroups != null && !beanFactory.containsBean(LOGGING_GROUPS_BEAN_NAME)) { - beanFactory.registerSingleton(LOGGING_GROUPS_BEAN_NAME, this.loggingGroups); + if (this.loggerGroups != null && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) { + beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups); } } @@ -269,11 +270,11 @@ public class LoggingApplicationListener implements GenericApplicationListener { */ protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) { new LoggingSystemProperties(environment).apply(); - this.loggingGroups = new LoggingGroups(this.loggingSystem); this.logFile = LogFile.get(environment); if (this.logFile != null) { this.logFile.applyToSystemProperties(); } + this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS); initializeEarlyLoggingLevel(environment); initializeSystem(environment, this.loggingSystem, this.logFile); initializeFinalLoggingLevels(environment, this.loggingSystem); @@ -321,70 +322,95 @@ public class LoggingApplicationListener implements GenericApplicationListener { } private void initializeFinalLoggingLevels(ConfigurableEnvironment environment, LoggingSystem system) { + bindLoggerGroups(environment); if (this.springBootLogging != null) { initializeLogLevel(system, this.springBootLogging); } setLogLevels(system, environment); } - protected void initializeLogLevel(LoggingSystem system, LogLevel level) { - LOG_LEVEL_LOGGERS.getOrDefault(level, Collections.emptyList()) - .forEach((logger) -> initializeLogLevel(system, level, logger)); + private void bindLoggerGroups(ConfigurableEnvironment environment) { + if (this.loggerGroups != null) { + Binder binder = Binder.get(environment); + binder.bind(LOGGING_GROUP, STRING_STRINGS_MAP).ifBound(this.loggerGroups::putAll); + } } - private void initializeLogLevel(LoggingSystem system, LogLevel level, String logger) { - List groupLoggers = DEFAULT_GROUP_LOGGERS.get(logger); - if (groupLoggers == null) { - system.setLogLevel(logger, level); - return; - } - this.loggingGroups.setLoggerGroup(logger, groupLoggers); - this.loggingGroups.setLoggerGroupLevel(logger, level); + /** + * Initialize loggers based on the {@link #setSpringBootLogging(LogLevel) + * springBootLogging} setting. + * @param system the logging system + * @param springBootLogging the spring boot logging level requested + * @deprecated since 2.2.0 in favor of + * {@link #initializeSpringBootLogging(LoggingSystem, LogLevel)} + */ + @Deprecated + protected void initializeLogLevel(LoggingSystem system, LogLevel springBootLogging) { + initializeSpringBootLogging(system, springBootLogging); } + /** + * Initialize loggers based on the {@link #setSpringBootLogging(LogLevel) + * springBootLogging} setting. By default this implementation will pick an appropriate + * set of loggers to configure based on the level. + * @param system the logging system + * @param springBootLogging the spring boot logging level requested + * @since 2.2.0 + */ + protected void initializeSpringBootLogging(LoggingSystem system, LogLevel springBootLogging) { + BiConsumer configurer = getLogLevelConfigurer(system); + SPRING_BOOT_LOGGING_LOGGERS.getOrDefault(springBootLogging, Collections.emptyList()) + .forEach((name) -> configureLogLevel(name, springBootLogging, configurer)); + } + + /** + * Set logging levels based on relevant {@link Environment} properties. + * @param system the logging system + * @param environment the environment + * @deprecated since 2.2.0 in favor of + * {@link #setLogLevels(LoggingSystem, ConfigurableEnvironment)} + */ + @Deprecated protected void setLogLevels(LoggingSystem system, Environment environment) { - if (!(environment instanceof ConfigurableEnvironment)) { - return; + if (environment instanceof ConfigurableEnvironment) { + setLogLevels(system, (ConfigurableEnvironment) environment); } - Binder binder = Binder.get(environment); - Map groups = getGroups(); - binder.bind(LOGGING_GROUP, STRING_STRINGS_MAP.withExistingValue(groups)); - Map levels = binder.bind(LOGGING_LEVEL, STRING_LOGLEVEL_MAP).orElseGet(Collections::emptyMap); - levels.forEach((name, level) -> { - String[] groupedNames = groups.get(name); - if (ObjectUtils.isEmpty(groupedNames)) { - setLogLevel(system, name, level); - } - else { - setLogLevel(groupedNames, level, name); - } - }); } - private Map getGroups() { - Map groups = new LinkedHashMap<>(); - DEFAULT_GROUP_LOGGERS.forEach((name, loggers) -> groups.put(name, StringUtils.toStringArray(loggers))); - return groups; + /** + * Set logging levels based on relevant {@link Environment} properties. + * @param system the logging system + * @param environment the environment + * @since 2.2.0 + */ + protected void setLogLevels(LoggingSystem system, ConfigurableEnvironment environment) { + BiConsumer customizer = getLogLevelConfigurer(system); + Binder binder = Binder.get(environment); + Map levels = binder.bind(LOGGING_LEVEL, STRING_LOGLEVEL_MAP).orElseGet(Collections::emptyMap); + levels.forEach((name, level) -> configureLogLevel(name, level, customizer)); } - private void setLogLevel(String[] names, LogLevel level, String groupName) { - try { - this.loggingGroups.setLoggerGroup(groupName, Arrays.asList(names)); - this.loggingGroups.setLoggerGroupLevel(groupName, level); - } - catch (RuntimeException ex) { - this.logger.error("Cannot set level '" + level + "' for '" + groupName + "'"); + private void configureLogLevel(String name, LogLevel level, BiConsumer configurer) { + if (this.loggerGroups != null) { + LoggerGroup group = this.loggerGroups.get(name); + if (group != null && group.hasMembers()) { + group.configureLogLevel(level, configurer); + return; + } } + configurer.accept(name, level); } - private void setLogLevel(LoggingSystem system, String name, LogLevel level) { - try { - name = name.equalsIgnoreCase(LoggingSystem.ROOT_LOGGER_NAME) ? null : name; - system.setLogLevel(name, level); - } - catch (RuntimeException ex) { - this.logger.error("Cannot set level '" + level + "' for '" + name + "'"); - } + private BiConsumer getLogLevelConfigurer(LoggingSystem system) { + return (name, level) -> { + try { + name = name.equalsIgnoreCase(LoggingSystem.ROOT_LOGGER_NAME) ? null : name; + system.setLogLevel(name, level); + } + catch (RuntimeException ex) { + this.logger.error("Cannot set level '" + level + "' for '" + name + "'"); + } + }; } private void registerShutdownHookIfNecessary(Environment environment, LoggingSystem loggingSystem) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggerGroup.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggerGroup.java new file mode 100644 index 00000000000..a16cf20deac --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggerGroup.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; + +/** + * A single logger group. + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.2.0 + */ +public final class LoggerGroup { + + private final String name; + + private final List members; + + private LogLevel configuredLevel; + + LoggerGroup(String name, List members) { + this.name = name; + this.members = Collections.unmodifiableList(new ArrayList<>(members)); + } + + public String getName() { + return this.name; + } + + public List getMembers() { + return this.members; + } + + public boolean hasMembers() { + return !this.members.isEmpty(); + } + + public LogLevel getConfiguredLevel() { + return this.configuredLevel; + } + + public void configureLogLevel(LogLevel level, BiConsumer configurer) { + this.configuredLevel = level; + this.members.forEach((name) -> configurer.accept(name, level)); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggerGroups.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggerGroups.java new file mode 100644 index 00000000000..a0790c8ea2f --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggerGroups.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Logger groups configured via the Spring Environment. + * + * @author HaiTao Zhang + * @author Phillip Webb + * @since 2.2.0 #see {@link LoggerGroup} + */ +public final class LoggerGroups implements Iterable { + + private final Map groups = new ConcurrentHashMap<>(); + + public LoggerGroups() { + } + + public LoggerGroups(Map> namesAndMembers) { + putAll(namesAndMembers); + } + + public void putAll(Map> namesAndMembers) { + namesAndMembers.forEach(this::put); + } + + private void put(String name, List members) { + put(new LoggerGroup(name, members)); + } + + private void put(LoggerGroup loggerGroup) { + this.groups.put(loggerGroup.getName(), loggerGroup); + } + + public LoggerGroup get(String name) { + return this.groups.get(name); + } + + @Override + public Iterator iterator() { + return this.groups.values().iterator(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingGroups.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingGroups.java deleted file mode 100644 index a6b1be08b3a..00000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingGroups.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.logging; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.springframework.util.Assert; - -/** - * Manage logger groups. - * - * @author HaiTao Zhang - * @since 2.2.0 - */ -public class LoggingGroups { - - private Map loggerGroupConfigurations; - - private Map> loggerGroups; - - private LoggingSystem loggingSystem; - - public LoggingGroups(LoggingSystem loggingSystem) { - this.loggerGroupConfigurations = new ConcurrentHashMap<>(); - this.loggerGroups = new ConcurrentHashMap<>(); - this.loggingSystem = loggingSystem; - } - - /** - * Associate a name to a list of logger's name to create a logger group. - * @param groupName name of the logger group - * @param members list of the members names - */ - public void setLoggerGroup(String groupName, List members) { - Assert.notNull(groupName, "Group name can not be null"); - Assert.notNull(members, "Members can not be null"); - this.loggerGroups.put(groupName, members); - } - - /** - * Set the logging level for a given logger group. - * @param groupName the name of the group to set - * @param level the log level ({@code null}) can be used to remove any custom level - * for the logger group and use the default configuration instead. - */ - public void setLoggerGroupLevel(String groupName, LogLevel level) { - Assert.notNull(groupName, "Group name can not be null"); - List members = this.loggerGroups.get(groupName); - members.forEach((member) -> this.loggingSystem - .setLogLevel(member.equalsIgnoreCase(LoggingSystem.ROOT_LOGGER_NAME) ? null : member, level)); - this.loggerGroupConfigurations.put(groupName, level); - } - - /** - * Checks whether a groupName is associated to a logger group. - * @param groupName name of the logger group - * @return a boolean stating true when groupName is associated with a group of loggers - */ - public boolean isGroup(String groupName) { - Assert.notNull(groupName, "Group name can not be null"); - return this.loggerGroups.containsKey(groupName); - } - - /** - * Get the all registered logger groups. - * @return a Set of the names of the logger groups - */ - public Set getLoggerGroupNames() { - synchronized (this) { - return this.loggerGroups.isEmpty() ? null : Collections.unmodifiableSet(this.loggerGroups.keySet()); - } - } - - /** - * Get a logger group's members. - * @param groupName name of the logger group - * @return list of the members names associated with this group - */ - public List getLoggerGroup(String groupName) { - Assert.notNull(groupName, "Group name can not be null"); - return Collections.unmodifiableList(this.loggerGroups.get(groupName)); - } - - /** - * Get a logger group's configured level. - * @param groupName name of the logger group - * @return the logger groups configured level - */ - public LogLevel getLoggerGroupConfiguredLevel(String groupName) { - Assert.notNull(groupName, "Group name can not be null"); - return this.loggerGroupConfigurations.get(groupName); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java index 6c701aa49cb..603c599efba 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java @@ -50,6 +50,7 @@ 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.LoggerGroups; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystemProperties; @@ -284,6 +285,8 @@ class LoggingApplicationListenerTests { this.loggerContext.getLogger("org.hibernate.SQL").debug("testdebugsqlgroup"); assertThat(this.output).contains("testdebugwebgroup"); assertThat(this.output).contains("testdebugsqlgroup"); + LoggerGroups loggerGroups = (LoggerGroups) ReflectionTestUtils.getField(this.initializer, "loggerGroups"); + assertThat(loggerGroups.get("web").getConfiguredLevel()).isEqualTo(LogLevel.DEBUG); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggerGroupsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggerGroupsTests.java new file mode 100644 index 00000000000..fb95f648257 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggerGroupsTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LoggerGroups} + * + * @author HaiTao Zhang + * @author Madhura Bhave + */ +class LoggerGroupsTests { + + private LoggingSystem loggingSystem = mock(LoggingSystem.class); + + @Test + void putAllShouldAddLoggerGroups() { + Map> groups = Collections.singletonMap("test", + Arrays.asList("test.member", "test.member2")); + LoggerGroups loggerGroups = new LoggerGroups(); + loggerGroups.putAll(groups); + LoggerGroup group = loggerGroups.get("test"); + assertThat(group.getMembers()).containsExactly("test.member", "test.member2"); + } + + @Test + void iteratorShouldReturnLoggerGroups() { + LoggerGroups groups = createLoggerGroups(); + assertThat(groups).hasSize(3); + assertThat(groups).extracting("name").containsExactlyInAnyOrder("test0", "test1", "test2"); + } + + private LoggerGroups createLoggerGroups() { + Map> groups = new LinkedHashMap<>(); + groups.put("test0", Arrays.asList("test0.member", "test0.member2")); + groups.put("test1", Arrays.asList("test1.member", "test1.member2")); + groups.put("test2", Arrays.asList("test2.member", "test2.member2")); + return new LoggerGroups(groups); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingGroupsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingGroupsTests.java deleted file mode 100644 index b0ab8ff21c2..00000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingGroupsTests.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.logging; - -import java.util.Arrays; - -import org.junit.jupiter.api.Test; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link LoggingGroups} - * - * @author HaiTao Zhang - */ -public class LoggingGroupsTests { - - private LoggingSystem loggingSystem = mock(LoggingSystem.class); - - @Test - void setLoggerGroupWithTheConfiguredLevelToAllMembers() { - LoggingGroups loggingGroups = new LoggingGroups(this.loggingSystem); - loggingGroups.setLoggerGroup("test", Arrays.asList("test.member", "test.member2")); - loggingGroups.setLoggerGroupLevel("test", LogLevel.DEBUG); - verify(this.loggingSystem).setLogLevel("test.member2", LogLevel.DEBUG); - verify(this.loggingSystem).setLogLevel("test.member", LogLevel.DEBUG); - } - -}