Merge pull request #17515 from htztomic

* pr/17515:
  Polish "Add support for configuring logging groups"
  Add support for configuring logging groups via endpoint

Closes gh-17515
This commit is contained in:
Madhura Bhave 2019-07-30 11:58:39 -07:00
commit 6e1fb5a1c6
11 changed files with 602 additions and 77 deletions

View File

@ -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

View File

@ -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.LoggerGroups;
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<LoggerGroups> springBootLoggerGroups) {
return new LoggersEndpoint(loggingSystem, springBootLoggerGroups.getIfAvailable(LoggerGroups::new));
}
static class OnEnabledLoggingSystemCondition extends SpringBootCondition {

View File

@ -17,14 +17,18 @@
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 java.util.Map;
import org.junit.jupiter.api.Test;
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.LoggerGroups;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
@ -54,9 +58,21 @@ 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<FieldDescriptor> groupLevelFields;
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 LoggingSystem loggingSystem;
@Autowired
private LoggerGroups loggerGroups;
@Test
void allLoggers() throws Exception {
given(this.loggingSystem.getSupportedLogLevels()).willReturn(EnumSet.allOf(LogLevel.class));
@ -66,8 +82,10 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest
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
@ -78,6 +96,12 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest
.andDo(MockMvcRestDocumentation.document("loggers/single", responseFields(levelFields)));
}
@Test
void loggerGroups() throws Exception {
this.mockMvc.perform(get("/actuator/loggers/test")).andExpect(status().isOk())
.andDo(MockMvcRestDocumentation.document("loggers/group", responseFields(groupLevelFields)));
}
@Test
void setLogLevel() throws Exception {
this.mockMvc
@ -89,6 +113,26 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest
verify(this.loggingSystem).setLogLevel("com.example", LogLevel.DEBUG);
}
@Test
void setLogLevelOfLoggerGroup() throws Exception {
this.mockMvc
.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.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 {
this.mockMvc
@ -102,8 +146,13 @@ class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTest
static class TestConfiguration {
@Bean
LoggersEndpoint endpoint(LoggingSystem loggingSystem) {
return new LoggersEndpoint(loggingSystem);
LoggersEndpoint endpoint(LoggingSystem loggingSystem, LoggerGroups groups) {
groups.putAll(getLoggerGroups());
return new LoggersEndpoint(loggingSystem, groups);
}
private Map<String, List<String>> getLoggerGroups() {
return Collections.singletonMap("test", Arrays.asList("test.member1", "test.member2"));
}
}

View File

@ -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,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.LoggerGroup;
import org.springframework.boot.logging.LoggerGroups;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -39,6 +42,7 @@ import org.springframework.util.Assert;
*
* @author Ben Hale
* @author Phillip Webb
* @author HaiTao Zhang
* @since 2.0.0
*/
@Endpoint(id = "loggers")
@ -46,13 +50,18 @@ public class LoggersEndpoint {
private final LoggingSystem loggingSystem;
private final LoggerGroups loggerGroups;
/**
* Create a new {@link LoggersEndpoint} instance.
* @param loggingSystem the logging system to expose
* @param loggerGroups the logger group to expose
*/
public LoggersEndpoint(LoggingSystem loggingSystem) {
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.loggerGroups = loggerGroups;
}
@ReadOperation
@ -64,19 +73,36 @@ public class LoggersEndpoint {
Map<String, Object> result = new LinkedHashMap<>();
result.put("levels", getLevels());
result.put("loggers", getLoggers(configurations));
result.put("groups", getGroups());
return result;
}
private Map<String, LoggerLevels> getGroups() {
Map<String, LoggerLevels> 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");
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 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");
LoggerGroup group = this.loggerGroups.get(name);
if (group != null && group.hasMembers()) {
group.configureLogLevel(configuredLevel, this.loggingSystem::setLogLevel);
return;
}
this.loggingSystem.setLogLevel(name, configuredLevel);
}
@ -88,7 +114,7 @@ public class LoggersEndpoint {
private Map<String, LoggerLevels> getLoggers(Collection<LoggerConfiguration> configurations) {
Map<String, LoggerLevels> 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;
}
@ -100,14 +126,11 @@ 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) {
protected final String getName(LogLevel level) {
return (level != null) ? level.name() : null;
}
@ -115,6 +138,32 @@ public class LoggersEndpoint {
return this.configuredLevel;
}
}
public static class GroupLoggerLevels extends LoggerLevels {
private List<String> members;
public GroupLoggerLevels(LogLevel configuredLevel, List<String> members) {
super(configuredLevel);
this.members = members;
}
public List<String> getMembers() {
return this.members;
}
}
public static class SingleLoggerLevels extends LoggerLevels {
private String effectiveLevel;
public SingleLoggerLevels(LoggerConfiguration configuration) {
super(configuration.getConfiguredLevel());
this.effectiveLevel = getName(configuration.getEffectiveLevel());
}
public String getEffectiveLevel() {
return this.effectiveLevel;
}

View File

@ -18,14 +18,19 @@ 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;
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.LoggerGroups;
import org.springframework.boot.logging.LoggingSystem;
import static org.assertj.core.api.Assertions.assertThat;
@ -38,46 +43,102 @@ 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 LoggerGroups loggerGroups;
@BeforeEach
void setup() {
Map<String, List<String>> 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")
void loggersShouldReturnLoggerConfigurations() {
void loggersShouldReturnLoggerConfigurationsWithNoLoggerGroups() {
given(this.loggingSystem.getLoggerConfigurations())
.willReturn(Collections.singletonList(new LoggerConfiguration("ROOT", null, LogLevel.DEBUG)));
given(this.loggingSystem.getSupportedLogLevels()).willReturn(EnumSet.allOf(LogLevel.class));
Map<String, Object> result = new LoggersEndpoint(this.loggingSystem).loggers();
Map<String, Object> result = new LoggersEndpoint(this.loggingSystem, new LoggerGroups()).loggers();
Map<String, LoggerLevels> loggers = (Map<String, LoggerLevels>) result.get("loggers");
Set<LogLevel> levels = (Set<LogLevel>) 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);
Map<String, LoggerGroups> groups = (Map<String, LoggerGroups>) result.get("groups");
assertThat(groups).isEmpty();
}
@Test
@SuppressWarnings("unchecked")
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<String, Object> result = new LoggersEndpoint(this.loggingSystem, this.loggerGroups).loggers();
Map<String, GroupLoggerLevels> loggerGroups = (Map<String, GroupLoggerLevels>) result.get("groups");
GroupLoggerLevels groupLevel = loggerGroups.get("test");
Map<String, LoggerLevels> loggers = (Map<String, LoggerLevels>) result.get("loggers");
Set<LogLevel> levels = (Set<LogLevel>) 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(loggerGroups).isNotNull();
assertThat(groupLevel.getConfiguredLevel()).isEqualTo("DEBUG");
assertThat(groupLevel.getMembers()).containsExactly("test.member");
}
@Test
void loggerLevelsWhenNameSpecifiedShouldReturnLevels() {
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.loggerGroups)
.loggerLevels("ROOT");
assertThat(levels.getConfiguredLevel()).isNull();
assertThat(levels.getEffectiveLevel()).isEqualTo("DEBUG");
}
@Test
void groupNameSpecifiedShouldReturnConfiguredLevelAndMembers() {
GroupLoggerLevels levels = (GroupLoggerLevels) new LoggersEndpoint(this.loggingSystem, this.loggerGroups)
.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);
new LoggersEndpoint(this.loggingSystem, this.loggerGroups).configureLogLevel("ROOT", LogLevel.DEBUG);
verify(this.loggingSystem).setLogLevel("ROOT", LogLevel.DEBUG);
}
@Test
void configureLogLevelWithNullSetsLevelOnLoggingSystemToNull() {
new LoggersEndpoint(this.loggingSystem).configureLogLevel("ROOT", null);
new LoggersEndpoint(this.loggingSystem, this.loggerGroups).configureLogLevel("ROOT", null);
verify(this.loggingSystem).setLogLevel("ROOT", null);
}
@Test
void configureLogLevelInLoggerGroupShouldSetLevelOnLoggingSystem() {
new LoggersEndpoint(this.loggingSystem, this.loggerGroups).configureLogLevel("test", LogLevel.DEBUG);
verify(this.loggingSystem).setLogLevel("test.member", LogLevel.DEBUG);
}
@Test
void configureLogLevelWithNullInLoggerGroupShouldSetLevelOnLoggingSystem() {
new LoggersEndpoint(this.loggingSystem, this.loggerGroups).configureLogLevel("test", null);
verify(this.loggingSystem).setLogLevel("test.member", null);
}
}

View File

@ -19,16 +19,22 @@ 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;
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.LoggerGroups;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
@ -50,6 +56,8 @@ import static org.mockito.Mockito.verifyZeroInteractions;
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author HaiTao Zhang
* @author Madhura Bhave
*/
class LoggersEndpointWebIntegrationTests {
@ -57,29 +65,35 @@ class LoggersEndpointWebIntegrationTests {
private LoggingSystem loggingSystem;
private LoggerGroups loggerGroups;
@BeforeEach
@AfterEach
void resetMocks(ConfigurableApplicationContext context, WebTestClient client) {
this.client = client;
this.loggingSystem = context.getBean(LoggingSystem.class);
this.loggerGroups = context.getBean(LoggerGroups.class);
Mockito.reset(this.loggingSystem);
given(this.loggingSystem.getSupportedLogLevels()).willReturn(EnumSet.allOf(LogLevel.class));
}
@WebEndpointTest
void getLoggerShouldReturnAllLoggerConfigurations() {
void getLoggerShouldReturnAllLoggerConfigurationsWithLoggerGroups() {
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()")
.isEqualTo(2).jsonPath("levels")
.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(2).jsonPath("groups.test.configuredLevel")
.isEqualTo("DEBUG");
}
@WebEndpointTest
void getLoggerShouldReturnLogLevels() {
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()
@ -88,10 +102,19 @@ class LoggersEndpointWebIntegrationTests {
}
@WebEndpointTest
void getLoggersWhenLoggerNotFoundShouldReturnNotFound() {
void getLoggersWhenLoggerAndLoggerGroupNotFoundShouldReturnNotFound() {
this.client.get().uri("/actuator/loggers/com.does.not.exist").exchange().expectStatus().isNotFound();
}
@WebEndpointTest
void getLoggerGroupShouldReturnConfiguredLogLevelAndMembers() {
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"))
.jsonPath("configuredLevel").isEqualTo("DEBUG");
}
@WebEndpointTest
void setLoggerUsingApplicationJsonShouldSetLogLevel() {
this.client.post().uri("/actuator/loggers/ROOT").contentType(MediaType.APPLICATION_JSON)
@ -108,7 +131,24 @@ class LoggersEndpointWebIntegrationTests {
}
@WebEndpointTest
void setLoggerWithWrongLogLevelResultInBadRequestResponse() {
void setLoggerGroupUsingActuatorV2JsonShouldSetLogLevel() {
this.client.post().uri("/actuator/loggers/test")
.contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON))
.body(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus().isNoContent();
verify(this.loggingSystem).setLogLevel("test.member1", LogLevel.DEBUG);
verify(this.loggingSystem).setLogLevel("test.member2", LogLevel.DEBUG);
}
@WebEndpointTest
void setLoggerGroupUsingApplicationJsonShouldSetLogLevel() {
this.client.post().uri("/actuator/loggers/test").contentType(MediaType.APPLICATION_JSON)
.body(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus().isNoContent();
verify(this.loggingSystem).setLogLevel("test.member1", LogLevel.DEBUG);
verify(this.loggingSystem).setLogLevel("test.member2", 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);
@ -130,6 +170,24 @@ class LoggersEndpointWebIntegrationTests {
verify(this.loggingSystem).setLogLevel("ROOT", null);
}
@WebEndpointTest
void setLoggerGroupWithNullLogLevel() {
this.client.post().uri("/actuator/loggers/test")
.contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON))
.body(Collections.singletonMap("configuredLevel", null)).exchange().expectStatus().isNoContent();
verify(this.loggingSystem).setLogLevel("test.member1", null);
verify(this.loggingSystem).setLogLevel("test.member2", null);
}
@WebEndpointTest
void setLoggerGroupWithNoLogLevel() {
this.client.post().uri("/actuator/loggers/test")
.contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)).body(Collections.emptyMap())
.exchange().expectStatus().isNoContent();
verify(this.loggingSystem).setLogLevel("test.member1", null);
verify(this.loggingSystem).setLogLevel("test.member2", null);
}
@WebEndpointTest
void logLevelForLoggerWithNameThatCouldBeMistakenForAPathExtension() {
given(this.loggingSystem.getLoggerConfiguration("com.png"))
@ -139,6 +197,19 @@ class LoggersEndpointWebIntegrationTests {
.jsonPath("effectiveLevel").isEqualTo("DEBUG");
}
@WebEndpointTest
void logLevelForLoggerGroupWithNameThatCouldBeMistakenForAPathExtension() {
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("png.member1", "png.member2"));
}
private void setLogLevelToDebug(String name) {
this.loggerGroups.get(name).configureLogLevel(LogLevel.DEBUG, (a, b) -> {
});
}
private JSONArray jsonArrayOf(Object... entries) {
JSONArray array = new JSONArray();
array.addAll(Arrays.asList(entries));
@ -154,8 +225,21 @@ class LoggersEndpointWebIntegrationTests {
}
@Bean
LoggersEndpoint endpoint(LoggingSystem loggingSystem) {
return new LoggersEndpoint(loggingSystem);
LoggerGroups loggingGroups() {
return getLoggerGroups();
}
private LoggerGroups getLoggerGroups() {
Map<String, List<String>> 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<LoggerGroups> loggingGroupsObjectProvider) {
return new LoggersEndpoint(loggingSystem, loggingGroupsObjectProvider.getIfAvailable());
}
}

View File

@ -17,10 +17,10 @@
package org.springframework.boot.context.logging;
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;
@ -36,6 +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.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;
@ -50,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;
@ -83,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)
*/
@ -95,8 +97,8 @@ public class LoggingApplicationListener implements GenericApplicationListener {
private static final Bindable<Map<String, LogLevel>> STRING_LOGLEVEL_MAP = Bindable.mapOf(String.class,
LogLevel.class);
private static final Bindable<Map<String, String[]>> STRING_STRINGS_MAP = Bindable.mapOf(String.class,
String[].class);
private static final Bindable<Map<String, List<String>>> STRING_STRINGS_MAP = Bindable
.of(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class).asMap());
/**
* The default order for the LoggingApplicationListener.
@ -126,6 +128,12 @@ public class LoggingApplicationListener implements GenericApplicationListener {
*/
public static final String LOGFILE_BEAN_NAME = "springBootLogFile";
/**
* The name of the{@link LoggerGroups} bean.
* @since 2.2.0
*/
public static final String LOGGER_GROUPS_BEAN_NAME = "springBootLoggerGroups";
private static final Map<String, List<String>> DEFAULT_GROUP_LOGGERS;
static {
MultiValueMap<String, String> loggers = new LinkedMultiValueMap<>();
@ -140,7 +148,7 @@ public class LoggingApplicationListener implements GenericApplicationListener {
DEFAULT_GROUP_LOGGERS = Collections.unmodifiableMap(loggers);
}
private static final Map<LogLevel, List<String>> LOG_LEVEL_LOGGERS;
private static final Map<LogLevel, List<String>> SPRING_BOOT_LOGGING_LOGGERS;
static {
MultiValueMap<LogLevel, String> loggers = new LinkedMultiValueMap<>();
loggers.add(LogLevel.DEBUG, "sql");
@ -151,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,
@ -168,6 +176,8 @@ public class LoggingApplicationListener implements GenericApplicationListener {
private LogFile logFile;
private LoggerGroups loggerGroups;
private int order = DEFAULT_ORDER;
private boolean parseArgs = true;
@ -235,6 +245,9 @@ public class LoggingApplicationListener implements GenericApplicationListener {
if (this.logFile != null && !beanFactory.containsBean(LOGFILE_BEAN_NAME)) {
beanFactory.registerSingleton(LOGFILE_BEAN_NAME, this.logFile);
}
if (this.loggerGroups != null && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) {
beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups);
}
}
private void onContextClosedEvent() {
@ -261,6 +274,7 @@ public class LoggingApplicationListener implements GenericApplicationListener {
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);
@ -308,65 +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 initializeLogLevel(LoggingSystem system, LogLevel level, String logger) {
List<String> groupLoggers = DEFAULT_GROUP_LOGGERS.get(logger);
if (groupLoggers == null) {
system.setLogLevel(logger, level);
return;
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);
}
groupLoggers.forEach((groupLogger) -> system.setLogLevel(groupLogger, 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<String, LogLevel> 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);
}
}
/**
* 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<String, LogLevel> customizer = getLogLevelConfigurer(system);
Binder binder = Binder.get(environment);
Map<String, String[]> groups = getGroups();
binder.bind(LOGGING_GROUP, STRING_STRINGS_MAP.withExistingValue(groups));
Map<String, LogLevel> 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);
levels.forEach((name, level) -> configureLogLevel(name, level, customizer));
}
private void configureLogLevel(String name, LogLevel level, BiConsumer<String, LogLevel> configurer) {
if (this.loggerGroups != null) {
LoggerGroup group = this.loggerGroups.get(name);
if (group != null && group.hasMembers()) {
group.configureLogLevel(level, configurer);
return;
}
else {
setLogLevel(system, groupedNames, level);
}
configurer.accept(name, level);
}
private BiConsumer<String, LogLevel> getLogLevelConfigurer(LoggingSystem system) {
return (name, level) -> {
try {
name = name.equalsIgnoreCase(LoggingSystem.ROOT_LOGGER_NAME) ? null : name;
system.setLogLevel(name, level);
}
});
}
private Map<String, String[]> getGroups() {
Map<String, String[]> groups = new LinkedHashMap<>();
DEFAULT_GROUP_LOGGERS.forEach((name, loggers) -> groups.put(name, StringUtils.toStringArray(loggers)));
return groups;
}
private void setLogLevel(LoggingSystem system, String[] names, LogLevel level) {
for (String name : names) {
setLogLevel(system, 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 + "'");
}
catch (RuntimeException ex) {
this.logger.error("Cannot set level '" + level + "' for '" + name + "'");
}
};
}
private void registerShutdownHookIfNecessary(Environment environment, LoggingSystem loggingSystem) {

View File

@ -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<String> members;
private LogLevel configuredLevel;
LoggerGroup(String name, List<String> members) {
this.name = name;
this.members = Collections.unmodifiableList(new ArrayList<>(members));
}
public String getName() {
return this.name;
}
public List<String> getMembers() {
return this.members;
}
public boolean hasMembers() {
return !this.members.isEmpty();
}
public LogLevel getConfiguredLevel() {
return this.configuredLevel;
}
public void configureLogLevel(LogLevel level, BiConsumer<String, LogLevel> configurer) {
this.configuredLevel = level;
this.members.forEach((name) -> configurer.accept(name, level));
}
}

View File

@ -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<LoggerGroup> {
private final Map<String, LoggerGroup> groups = new ConcurrentHashMap<>();
public LoggerGroups() {
}
public LoggerGroups(Map<String, List<String>> namesAndMembers) {
putAll(namesAndMembers);
}
public void putAll(Map<String, List<String>> namesAndMembers) {
namesAndMembers.forEach(this::put);
}
private void put(String name, List<String> 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<LoggerGroup> iterator() {
return this.groups.values().iterator();
}
}

View File

@ -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

View File

@ -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<String, List<String>> 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<String, List<String>> 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);
}
}