Allow TraceWebFilter to trace more attributes

Update TraceWebFilter to optionally trace more details from the
HttpServletRequest/HttpServletResponse. The `management.trace.include`
property can be used to change what aspects are logged.

Closes gh-3948
This commit is contained in:
Wallace Wadge 2015-09-12 21:44:20 +02:00 committed by Phillip Webb
parent 143536f72d
commit e3315d2252
6 changed files with 331 additions and 53 deletions

View File

@ -22,12 +22,14 @@ import javax.servlet.ServletRegistration;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.trace.TraceProperties;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.DispatcherServlet;
@ -39,6 +41,7 @@ import org.springframework.web.servlet.DispatcherServlet;
*/
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, ServletRegistration.class })
@AutoConfigureAfter(TraceRepositoryAutoConfiguration.class)
@EnableConfigurationProperties(TraceProperties.class)
public class TraceWebFilterAutoConfiguration {
@Autowired
@ -50,10 +53,15 @@ public class TraceWebFilterAutoConfiguration {
@Value("${management.dump_requests:false}")
private boolean dumpRequests;
@Autowired
TraceProperties traceProperties = new TraceProperties();
@Bean
public WebRequestTraceFilter webRequestLoggingFilter(BeanFactory beanFactory) {
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.traceRepository);
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.traceRepository,
this.traceProperties);
filter.setDumpRequests(this.dumpRequests);
if (this.errorAttributes != null) {
filter.setErrorAttributes(this.errorAttributes);
}

View File

@ -0,0 +1,147 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.trace;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for tracing.
*
* @author Wallace Wadge
* @author Phillip Webb
* @since 1.3.0
*/
@ConfigurationProperties(prefix = "management.trace")
public class TraceProperties {
private static final Set<Include> DEFAULT_INCLUDES;
static {
Set<Include> defaultIncludes = new LinkedHashSet<Include>();
defaultIncludes.add(Include.REQUEST_HEADERS);
defaultIncludes.add(Include.RESPONSE_HEADERS);
defaultIncludes.add(Include.ERRORS);
DEFAULT_INCLUDES = Collections.unmodifiableSet(defaultIncludes);
}
private static final int DEFAULT_MAX_CONTENT_LENGTH = 32768;
/**
* Items to included in the trace. Defaults to request/response headers and errors.
*/
private Set<Include> include = new HashSet<Include>(DEFAULT_INCLUDES);
/**
* Maximum number of content bytes that can be traced before being truncated (-1 for
* unlimited).
*/
private int maxContentLength = DEFAULT_MAX_CONTENT_LENGTH;
public Set<Include> getInclude() {
return this.include;
}
public void setInclude(Set<Include> include) {
this.include = include;
}
public int getMaxContentLength() {
return this.maxContentLength;
}
public void setMaxContentLength(int maxContentLength) {
this.maxContentLength = maxContentLength;
}
/**
* Include options for tracing.
*/
public enum Include {
/**
* Include request headers.
*/
REQUEST_HEADERS,
/**
* Include response headers.
*/
RESPONSE_HEADERS,
/**
* Include errors (if any).
*/
ERRORS,
/**
* Include path info.
*/
PATH_INFO,
/**
* Include the translated path.
*/
PATH_TRANSLATED,
/**
* Include the context path.
*/
CONTEXT_PATH,
/**
* Include the user principal.
*/
USER_PRINCIPAL,
/**
* Include the parameters.
*/
PARAMETERS,
/**
* Include the query string.
*/
QUERY_STRING,
/**
* Include the authentication type.
*/
AUTH_TYPE,
/**
* Include the remote address.
*/
REMOTE_ADDRESS,
/**
* Include the session ID.
*/
SESSION_ID,
/**
* Include the remote user.
*/
REMOTE_USER,
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.boot.actuate.trace;
import java.io.IOException;
import java.security.Principal;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
@ -28,12 +29,13 @@ import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.actuate.trace.TraceProperties.Include;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.core.Ordered;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.filter.OncePerRequestFilter;
@ -41,6 +43,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
* Servlet {@link Filter} that logs all requests to a {@link TraceRepository}.
*
* @author Dave Syer
* @author Wallace Wadge
*/
public class WebRequestTraceFilter extends OncePerRequestFilter implements Ordered {
@ -52,16 +55,31 @@ public class WebRequestTraceFilter extends OncePerRequestFilter implements Order
// enriched headers, but users can add stuff after this if they want to
private int order = Ordered.LOWEST_PRECEDENCE - 10;
private final TraceRepository traceRepository;
private final TraceRepository repository;
private ErrorAttributes errorAttributes;
private final TraceProperties properties;
/**
* Create a new {@link WebRequestTraceFilter} instance.
* @param traceRepository the trace repository
* @param traceRepository the trace repository.
* @deprecated since 1.3.0 in favor of
* {@link #WebRequestTraceFilter(TraceRepository, TraceProperties)}
*/
@Deprecated
public WebRequestTraceFilter(TraceRepository traceRepository) {
this.traceRepository = traceRepository;
this(traceRepository, new TraceProperties());
}
/**
* Create a new {@link WebRequestTraceFilter} instance.
* @param repository the trace repository
* @param properties the trace properties
*/
public WebRequestTraceFilter(TraceRepository repository, TraceProperties properties) {
this.repository = repository;
this.properties = properties;
}
/**
@ -86,44 +104,54 @@ public class WebRequestTraceFilter extends OncePerRequestFilter implements Order
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Map<String, Object> trace = getTrace(request);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Processing request " + request.getMethod() + " "
+ request.getRequestURI());
if (this.dumpRequests) {
@SuppressWarnings("unchecked")
Map<String, Object> headers = (Map<String, Object>) trace.get("headers");
this.logger.trace("Headers: " + headers);
}
}
logTrace(request, trace);
try {
filterChain.doFilter(request, response);
}
finally {
enhanceTrace(trace, response);
this.traceRepository.add(trace);
this.repository.add(trace);
}
}
protected void enhanceTrace(Map<String, Object> trace, HttpServletResponse response) {
Map<String, String> headers = new LinkedHashMap<String, String>();
for (String header : response.getHeaderNames()) {
String value = response.getHeader(header);
headers.put(header, value);
}
headers.put("status", "" + response.getStatus());
@SuppressWarnings("unchecked")
Map<String, Object> allHeaders = (Map<String, Object>) trace.get("headers");
allHeaders.put("response", headers);
}
protected Map<String, Object> getTrace(HttpServletRequest request) {
HttpSession session = request.getSession(false);
Throwable exception = (Throwable) request
.getAttribute("javax.servlet.error.exception");
Principal userPrincipal = request.getUserPrincipal();
Map<String, Object> trace = new LinkedHashMap<String, Object>();
Map<String, Object> headers = new LinkedHashMap<String, Object>();
trace.put("method", request.getMethod());
trace.put("path", request.getRequestURI());
trace.put("headers", headers);
if (isIncluded(Include.REQUEST_HEADERS)) {
headers.put("request", getRequestHeaders(request));
}
add(trace, Include.PATH_INFO, "pathInfo", request.getPathInfo());
add(trace, Include.PATH_TRANSLATED, "pathTranslated",
request.getPathTranslated());
add(trace, Include.CONTEXT_PATH, "contextPath", request.getContextPath());
add(trace, Include.USER_PRINCIPAL, "userPrincipal",
(userPrincipal == null ? null : userPrincipal.getName()));
add(trace, Include.PARAMETERS, "parameters", request.getParameterMap());
add(trace, Include.QUERY_STRING, "query", request.getQueryString());
add(trace, Include.AUTH_TYPE, "authType", request.getAuthType());
add(trace, Include.REMOTE_ADDRESS, "remoteAddress", request.getRemoteAddr());
add(trace, Include.SESSION_ID, "sessionId",
(session == null ? null : session.getId()));
add(trace, Include.REMOTE_USER, "remoteUser", request.getRemoteUser());
if (isIncluded(Include.ERRORS) && exception != null
&& this.errorAttributes != null) {
trace.put("error", this.errorAttributes
.getErrorAttributes(new ServletRequestAttributes(request), true));
}
return trace;
}
private Map<String, Object> getRequestHeaders(HttpServletRequest request) {
Map<String, Object> headers = new LinkedHashMap<String, Object>();
Enumeration<String> names = request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
List<String> values = Collections.list(request.getHeaders(name));
@ -135,23 +163,45 @@ public class WebRequestTraceFilter extends OncePerRequestFilter implements Order
value = "";
}
headers.put(name, value);
}
return headers;
}
@SuppressWarnings("unchecked")
protected void enhanceTrace(Map<String, Object> trace, HttpServletResponse response) {
Map<String, Object> headers = (Map<String, Object>) trace.get("headers");
headers.put("response", getResponseHeaders(response));
}
private Map<String, String> getResponseHeaders(HttpServletResponse response) {
Map<String, String> headers = new LinkedHashMap<String, String>();
for (String header : response.getHeaderNames()) {
String value = response.getHeader(header);
headers.put(header, value);
}
Map<String, Object> trace = new LinkedHashMap<String, Object>();
Map<String, Object> allHeaders = new LinkedHashMap<String, Object>();
allHeaders.put("request", headers);
trace.put("method", request.getMethod());
trace.put("path", request.getRequestURI());
trace.put("headers", allHeaders);
Throwable exception = (Throwable) request
.getAttribute("javax.servlet.error.exception");
if (exception != null && this.errorAttributes != null) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
Map<String, Object> error = this.errorAttributes
.getErrorAttributes(requestAttributes, true);
trace.put("error", error);
headers.put("status", "" + response.getStatus());
return headers;
}
private void logTrace(HttpServletRequest request, Map<String, Object> trace) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Processing request " + request.getMethod() + " "
+ request.getRequestURI());
if (this.dumpRequests) {
this.logger.trace("Headers: " + trace.get("headers"));
}
}
return trace;
}
private void add(Map<String, Object> trace, Include include, String name,
Object value) {
if (isIncluded(include) && value != null) {
trace.put(name, value);
}
}
private boolean isIncluded(Include include) {
return this.properties.getInclude().contains(include);
}
public void setErrorAttributes(ErrorAttributes errorAttributes) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2015 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.
@ -16,9 +16,21 @@
package org.springframework.boot.actuate.trace;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.security.Principal;
import java.util.EnumSet;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import org.junit.Test;
import org.springframework.boot.actuate.trace.TraceProperties.Include;
import org.springframework.boot.autoconfigure.web.DefaultErrorAttributes;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
@ -29,35 +41,88 @@ import static org.junit.Assert.assertEquals;
* Tests for {@link WebRequestTraceFilter}.
*
* @author Dave Syer
* @author Wallace Wadge
* @author Phillip Webb
*/
public class WebRequestTraceFilterTests {
private final WebRequestTraceFilter filter = new WebRequestTraceFilter(
new InMemoryTraceRepository());
private final InMemoryTraceRepository repository = new InMemoryTraceRepository();
private TraceProperties properties = new TraceProperties();
private WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository,
this.properties);
@Test
public void filterDumpsRequest() {
@SuppressWarnings("unchecked")
public void filterAddsTraceWithDefaultIncludes() {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.addHeader("Accept", "application/json");
Map<String, Object> trace = this.filter.getTrace(request);
assertEquals("GET", trace.get("method"));
assertEquals("/foo", trace.get("path"));
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) trace.get("headers");
assertEquals("{Accept=application/json}", map.get("request").toString());
}
@Test
public void filterDumpsResponse() {
@SuppressWarnings({ "rawtypes", "unchecked" })
public void filterAddsTraceWithCustomIncludes() throws IOException, ServletException {
this.properties.setInclude(EnumSet.allOf(Include.class));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.addHeader("Accept", "application/json");
request.setContextPath("some.context.path");
request.setContent("Hello, World!".getBytes());
request.setRemoteAddr("some.remote.addr");
request.setQueryString("some.query.string");
request.setParameter("param", "paramvalue");
File tmp = File.createTempFile("spring-boot", "tmp");
String url = tmp.toURI().toURL().toString();
request.setPathInfo(url);
tmp.deleteOnExit();
Cookie cookie = new Cookie("testCookie", "testValue");
request.setCookies(cookie);
request.setAuthType("authType");
Principal principal = new Principal() {
@Override
public String getName() {
return "principalTest";
}
};
request.setUserPrincipal(principal);
MockHttpServletResponse response = new MockHttpServletResponse();
response.addHeader("Content-Type", "application/json");
Map<String, Object> trace = this.filter.getTrace(request);
this.filter.enhanceTrace(trace, response);
@SuppressWarnings("unchecked")
this.filter.doFilterInternal(request, response, new FilterChain() {
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
BufferedReader bufferedReader = request.getReader();
while (bufferedReader.readLine() != null) {
// read the contents as normal (forces cache to fill up)
}
response.getWriter().println("Goodbye, World!");
}
});
assertEquals(1, this.repository.findAll().size());
Map<String, Object> trace = this.repository.findAll().iterator().next().getInfo();
Map<String, Object> map = (Map<String, Object>) trace.get("headers");
assertEquals("{Content-Type=application/json, status=200}",
map.get("response").toString());
assertEquals("GET", trace.get("method"));
assertEquals("/foo", trace.get("path"));
assertEquals("paramvalue",
((String[]) ((Map) trace.get("parameters")).get("param"))[0]);
assertEquals("some.remote.addr", trace.get("remoteAddress"));
assertEquals("some.query.string", trace.get("query"));
assertEquals(principal.getName(), trace.get("userPrincipal"));
assertEquals("some.context.path", trace.get("contextPath"));
assertEquals(url, trace.get("pathInfo"));
assertEquals("authType", trace.get("authType"));
assertEquals("{Accept=application/json}", map.get("request").toString());
}
@Test

View File

@ -784,6 +784,9 @@ content into your application; rather pick only the properties that you need.
management.health.solr.enabled=true
management.health.status.order=DOWN, OUT_OF_SERVICE, UNKNOWN, UP
# TRACING (({sc-spring-boot-actuator}/trace/TraceProperties.{sc-ext}[TraceProperties])
management.trace.include=request-headers,response-headers,errors # See TraceProperties.Include for options
# MVC ONLY ENDPOINTS
endpoints.jolokia.path=/jolokia
endpoints.jolokia.sensitive=true

View File

@ -22,3 +22,8 @@ info.group: @project.groupId@
info.artifact: @project.artifactId@
info.name: @project.name@
info.version: @project.version@
management.trace.include=REQUEST_HEADERS,RESPONSE_HEADERS,ERRORS,PATH_INFO,\
PATH_TRANSLATED,CONTEXT_PATH,USER_PRINCIPAL,PARAMETERS,QUERY_STRING,AUTH_TYPE,\
REMOTE_ADDRESS,SESSION_ID,REMOTE_USER