Commit f2511b7f authored by Brian Clozel's avatar Brian Clozel

Improve Web DEBUG logging output configuration

Since SPR-16946, Spring Framework revisited the DEBUG logging output
developers get while working on Spring MVC and Spring WebFlux
applications.

This commit aligns to those changes where DEBUG output was produced
in Spring Boot (especially in `DefaultErrorWebExceptionHandler`).

This also enables DEBUG logging on the related packages when running an
application with Spring Boot Developer Tools, providing a better
development experience.

This is also adding the new `spring.insights.web.log-request-details`
configuration property, which logs additional information about the
incoming requests at the DEBUG and TRACE levels. Since that information
can be sensitive (e.g. credentials, tokens, etc.), this property is not
enabled by default nor activated by the Developer Tools.

Closes: gh-13511
parent 13f08e4c
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -22,7 +22,9 @@ import org.springframework.boot.autoconfigure.AutoConfigureAfter; ...@@ -22,7 +22,9 @@ import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.insights.InsightsProperties;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
...@@ -64,4 +66,18 @@ public class CodecsAutoConfiguration { ...@@ -64,4 +66,18 @@ public class CodecsAutoConfiguration {
} }
@Configuration
@EnableConfigurationProperties(InsightsProperties.class)
static class LoggingCodecConfiguration {
@Bean
public CodecCustomizer loggingCodecCustomizer(InsightsProperties properties) {
return (configurer) -> {
configurer.defaultCodecs().enableLoggingRequestDetails(
properties.getWeb().isLogRequestDetails());
};
}
}
} }
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.insights;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* {@link ConfigurationProperties properties} for .
*
* @author Brian Clozel
* @since 2.1.0
*/
@ConfigurationProperties(prefix = "spring.insights")
public class InsightsProperties {
private final Web web = new Web();
public Web getWeb() {
return this.web;
}
public static class Web {
/**
* Whether logging of (potentially sensitive) request details at DEBUG and TRACE
* level is allowed.
*/
private boolean logRequestDetails = false;
public boolean isLogRequestDetails() {
return this.logRequestDetails;
}
public void setLogRequestDetails(boolean logRequestDetails) {
this.logRequestDetails = logRequestDetails;
}
}
}
...@@ -20,10 +20,8 @@ import java.util.Collections; ...@@ -20,10 +20,8 @@ import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiConsumer;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
...@@ -31,6 +29,7 @@ import org.springframework.boot.autoconfigure.web.ErrorProperties; ...@@ -31,6 +29,7 @@ import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpLogging;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
...@@ -39,7 +38,6 @@ import org.springframework.web.reactive.function.server.RequestPredicate; ...@@ -39,7 +38,6 @@ import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import static org.springframework.web.reactive.function.server.RequestPredicates.all; import static org.springframework.web.reactive.function.server.RequestPredicates.all;
import static org.springframework.web.reactive.function.server.RouterFunctions.route; import static org.springframework.web.reactive.function.server.RouterFunctions.route;
...@@ -79,8 +77,8 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa ...@@ -79,8 +77,8 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
private static final Map<HttpStatus.Series, String> SERIES_VIEWS; private static final Map<HttpStatus.Series, String> SERIES_VIEWS;
private static final Log logger = LogFactory private static final Log logger = HttpLogging
.getLog(DefaultErrorWebExceptionHandler.class); .forLogName(DefaultErrorWebExceptionHandler.class);
static { static {
Map<HttpStatus.Series, String> views = new EnumMap<>(HttpStatus.Series.class); Map<HttpStatus.Series, String> views = new EnumMap<>(HttpStatus.Series.class);
...@@ -206,30 +204,15 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa ...@@ -206,30 +204,15 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
*/ */
protected void logError(ServerRequest request, HttpStatus errorStatus) { protected void logError(ServerRequest request, HttpStatus errorStatus) {
Throwable ex = getError(request); Throwable ex = getError(request);
log(request, ex, (errorStatus.is5xxServerError() ? logger::error : logger::warn)); if (logger.isDebugEnabled()) {
} logger.debug(request.exchange().getLogPrefix() + formatError(ex, request));
private void log(ServerRequest request, Throwable ex,
BiConsumer<Object, Throwable> logger) {
if (ex instanceof ResponseStatusException) {
logger.accept(buildMessage(request, ex), null);
}
else {
logger.accept(buildMessage(request, null), ex);
} }
} }
private String buildMessage(ServerRequest request, Throwable ex) { private String formatError(Throwable ex, ServerRequest request) {
StringBuilder message = new StringBuilder("Failed to handle request ["); String reason = ex.getClass().getSimpleName() + ": " + ex.getMessage();
message.append(request.methodName()); return "Resolved [" + reason + "] for HTTP " + request.methodName() + " "
message.append(" "); + request.path();
message.append(request.uri());
message.append("]");
if (ex != null) {
message.append(": ");
message.append(ex.getMessage());
}
return message.toString();
} }
} }
...@@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean ...@@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.insights.InsightsProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
...@@ -81,24 +82,31 @@ public class DispatcherServletAutoConfiguration { ...@@ -81,24 +82,31 @@ public class DispatcherServletAutoConfiguration {
@Configuration @Configuration
@Conditional(DefaultDispatcherServletCondition.class) @Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class) @ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class) @EnableConfigurationProperties({ WebMvcProperties.class, InsightsProperties.class })
protected static class DispatcherServletConfiguration { protected static class DispatcherServletConfiguration {
private final WebMvcProperties webMvcProperties; private final WebMvcProperties webMvcProperties;
public DispatcherServletConfiguration(WebMvcProperties webMvcProperties) { private final InsightsProperties insightsProperties;
public DispatcherServletConfiguration(WebMvcProperties webMvcProperties,
InsightsProperties insightsProperties) {
this.webMvcProperties = webMvcProperties; this.webMvcProperties = webMvcProperties;
this.insightsProperties = insightsProperties;
} }
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet() { public DispatcherServlet dispatcherServlet() {
DispatcherServlet dispatcherServlet = new DispatcherServlet(); DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setShouldHandleFailure(true);
dispatcherServlet.setDispatchOptionsRequest( dispatcherServlet.setDispatchOptionsRequest(
this.webMvcProperties.isDispatchOptionsRequest()); this.webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest( dispatcherServlet.setDispatchTraceRequest(
this.webMvcProperties.isDispatchTraceRequest()); this.webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound( dispatcherServlet.setThrowExceptionIfNoHandlerFound(
this.webMvcProperties.isThrowExceptionIfNoHandlerFound()); this.webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setEnableLoggingRequestDetails(
this.insightsProperties.getWeb().isLogRequestDetails());
return dispatcherServlet; return dispatcherServlet;
} }
......
...@@ -30,7 +30,6 @@ import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfig ...@@ -30,7 +30,6 @@ import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfig
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
...@@ -44,10 +43,7 @@ import org.springframework.web.server.ResponseStatusException; ...@@ -44,10 +43,7 @@ import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
/** /**
* Integration tests for {@link DefaultErrorWebExceptionHandler} * Integration tests for {@link DefaultErrorWebExceptionHandler}
...@@ -70,9 +66,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests { ...@@ -70,9 +66,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests {
@Rule @Rule
public ExpectedException thrown = ExpectedException.none(); public ExpectedException thrown = ExpectedException.none();
@Rule
public OutputCapture output = new OutputCapture();
@Test @Test
public void jsonError() { public void jsonError() {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
...@@ -85,8 +78,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests { ...@@ -85,8 +78,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests {
.jsonPath("path").isEqualTo(("/")).jsonPath("message") .jsonPath("path").isEqualTo(("/")).jsonPath("message")
.isEqualTo("Expected!").jsonPath("exception").doesNotExist() .isEqualTo("Expected!").jsonPath("exception").doesNotExist()
.jsonPath("trace").doesNotExist(); .jsonPath("trace").doesNotExist();
this.output.expect(allOf(containsString("Failed to handle request [GET /]"),
containsString("IllegalStateException")));
}); });
} }
...@@ -112,8 +103,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests { ...@@ -112,8 +103,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests {
.expectHeader().contentType(MediaType.TEXT_HTML) .expectHeader().contentType(MediaType.TEXT_HTML)
.expectBody(String.class).returnResult().getResponseBody(); .expectBody(String.class).returnResult().getResponseBody();
assertThat(body).contains("status: 500").contains("message: Expected!"); assertThat(body).contains("status: 500").contains("message: Expected!");
this.output.expect(allOf(containsString("Failed to handle request [GET /]"),
containsString("IllegalStateException")));
}); });
} }
...@@ -129,9 +118,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests { ...@@ -129,9 +118,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests {
.isEqualTo(("/bind")).jsonPath("exception").doesNotExist() .isEqualTo(("/bind")).jsonPath("exception").doesNotExist()
.jsonPath("errors").isArray().jsonPath("message").isNotEmpty(); .jsonPath("errors").isArray().jsonPath("message").isNotEmpty();
}); });
this.output.expect(allOf(containsString("Failed to handle request [POST /bind]"),
containsString("Validation failed for argument"),
containsString("Field error in object 'dummyBody' on field 'content'")));
} }
@Test @Test
...@@ -197,7 +183,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests { ...@@ -197,7 +183,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests {
.isEqualTo(HttpStatus.BAD_REQUEST.getReasonPhrase()) .isEqualTo(HttpStatus.BAD_REQUEST.getReasonPhrase())
.jsonPath("exception") .jsonPath("exception")
.isEqualTo(ResponseStatusException.class.getName()); .isEqualTo(ResponseStatusException.class.getName());
this.output.expect(not(containsString("ResponseStatusException")));
}); });
} }
...@@ -215,9 +200,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests { ...@@ -215,9 +200,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests {
.returnResult().getResponseBody(); .returnResult().getResponseBody();
assertThat(body).contains("Whitelabel Error Page") assertThat(body).contains("Whitelabel Error Page")
.contains("<div>Expected!</div>"); .contains("<div>Expected!</div>");
this.output.expect(
allOf(containsString("Failed to handle request [GET /]"),
containsString("IllegalStateException")));
}); });
} }
...@@ -235,9 +217,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests { ...@@ -235,9 +217,6 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests {
.returnResult().getResponseBody(); .returnResult().getResponseBody();
assertThat(body).contains("Whitelabel Error Page") assertThat(body).contains("Whitelabel Error Page")
.doesNotContain("<script>").contains("&lt;script&gt;"); .doesNotContain("<script>").contains("&lt;script&gt;");
this.output.expect(
allOf(containsString("Failed to handle request [GET /html]"),
containsString("IllegalStateException")));
}); });
} }
......
...@@ -170,12 +170,16 @@ public class DispatcherServletAutoConfigurationTests { ...@@ -170,12 +170,16 @@ public class DispatcherServletAutoConfigurationTests {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
DispatcherServlet dispatcherServlet = context DispatcherServlet dispatcherServlet = context
.getBean(DispatcherServlet.class); .getBean(DispatcherServlet.class);
assertThat(dispatcherServlet).extracting("shouldHandleFailure")
.containsExactly(true);
assertThat(dispatcherServlet).extracting("throwExceptionIfNoHandlerFound") assertThat(dispatcherServlet).extracting("throwExceptionIfNoHandlerFound")
.containsExactly(false); .containsExactly(false);
assertThat(dispatcherServlet).extracting("dispatchOptionsRequest") assertThat(dispatcherServlet).extracting("dispatchOptionsRequest")
.containsExactly(true); .containsExactly(true);
assertThat(dispatcherServlet).extracting("dispatchTraceRequest") assertThat(dispatcherServlet).extracting("dispatchTraceRequest")
.containsExactly(false); .containsExactly(false);
assertThat(dispatcherServlet).extracting("enableLoggingRequestDetails")
.containsExactly(false);
assertThat(new DirectFieldAccessor( assertThat(new DirectFieldAccessor(
context.getBean("dispatcherServletRegistration")) context.getBean("dispatcherServletRegistration"))
.getPropertyValue("loadOnStartup")).isEqualTo(-1); .getPropertyValue("loadOnStartup")).isEqualTo(-1);
......
...@@ -59,6 +59,9 @@ public class DevToolsPropertyDefaultsPostProcessor implements EnvironmentPostPro ...@@ -59,6 +59,9 @@ public class DevToolsPropertyDefaultsPostProcessor implements EnvironmentPostPro
devToolsProperties.put("server.error.include-stacktrace", "ALWAYS"); devToolsProperties.put("server.error.include-stacktrace", "ALWAYS");
devToolsProperties.put("server.servlet.jsp.init-parameters.development", "true"); devToolsProperties.put("server.servlet.jsp.init-parameters.development", "true");
devToolsProperties.put("spring.reactor.stacktrace-mode.enabled", "true"); devToolsProperties.put("spring.reactor.stacktrace-mode.enabled", "true");
devToolsProperties.put("logging.level.org.springframework.web", "DEBUG");
devToolsProperties.put("logging.level.org.springframework.core.codec", "DEBUG");
devToolsProperties.put("logging.level.org.springframework.http", "DEBUG");
PROPERTIES = Collections.unmodifiableMap(devToolsProperties); PROPERTIES = Collections.unmodifiableMap(devToolsProperties);
} }
......
...@@ -92,6 +92,9 @@ content into your application. Rather, pick only the properties that you need. ...@@ -92,6 +92,9 @@ content into your application. Rather, pick only the properties that you need.
# HAZELCAST ({sc-spring-boot-autoconfigure}/hazelcast/HazelcastProperties.{sc-ext}[HazelcastProperties]) # HAZELCAST ({sc-spring-boot-autoconfigure}/hazelcast/HazelcastProperties.{sc-ext}[HazelcastProperties])
spring.hazelcast.config= # The location of the configuration file to use to initialize Hazelcast. spring.hazelcast.config= # The location of the configuration file to use to initialize Hazelcast.
# INSIGHTS
spring.insights.web.log-request-details=false # Whether logging of (potentially sensitive) request details at DEBUG and TRACE level is allowed.
# PROJECT INFORMATION ({sc-spring-boot-autoconfigure}/info/ProjectInfoProperties.{sc-ext}[ProjectInfoProperties]) # PROJECT INFORMATION ({sc-spring-boot-autoconfigure}/info/ProjectInfoProperties.{sc-ext}[ProjectInfoProperties])
spring.info.build.location=classpath:META-INF/build-info.properties # Location of the generated build-info.properties file. spring.info.build.location=classpath:META-INF/build-info.properties # Location of the generated build-info.properties file.
spring.info.git.location=classpath:git.properties # Location of the generated git.properties file. spring.info.git.location=classpath:git.properties # Location of the generated git.properties file.
......
...@@ -781,6 +781,13 @@ For example, Thymeleaf offers the `spring.thymeleaf.cache` property. Rather than ...@@ -781,6 +781,13 @@ For example, Thymeleaf offers the `spring.thymeleaf.cache` property. Rather than
to set these properties manually, the `spring-boot-devtools` module automatically applies to set these properties manually, the `spring-boot-devtools` module automatically applies
sensible development-time configuration. sensible development-time configuration.
Because you need more information about web requests while developing Spring MVC and
Spring WebFlux applications, developer tools will enable DEBUG logging for the
Spring Framework web infrastructure. This will give you information about the incoming
request, which handler is processing it, the response outcome, etc. If you wish to log
all request details (including potentially sensitive information), you can turn on
the `spring.insights.web.log-request-details` configuration property.
TIP: For a complete list of the properties that are applied by the devtools, see TIP: For a complete list of the properties that are applied by the devtools, see
{sc-spring-boot-devtools}/env/DevToolsPropertyDefaultsPostProcessor.{sc-ext}[DevToolsPropertyDefaultsPostProcessor]. {sc-spring-boot-devtools}/env/DevToolsPropertyDefaultsPostProcessor.{sc-ext}[DevToolsPropertyDefaultsPostProcessor].
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment