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 0b370d5495..96d419ae26 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 8b2e18b0c8..17ee607c84 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 ac57556576..25631d98ef 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 a83034508e..29b766abd2 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 97de049835..9ce47c0814 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 f69202c833..9dab14b12d 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 0000000000..a6b1be08b3 --- /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 0000000000..b0ab8ff21c --- /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); + } + +}