diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/TraceWebFilterAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/TraceWebFilterAutoConfiguration.java index aa25210e2f0..0835005774e 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/TraceWebFilterAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/TraceWebFilterAutoConfiguration.java @@ -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); } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/TraceProperties.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/TraceProperties.java new file mode 100644 index 00000000000..4eb45cd0a77 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/TraceProperties.java @@ -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 DEFAULT_INCLUDES; + + static { + Set defaultIncludes = new LinkedHashSet(); + 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 = new HashSet(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 getInclude() { + return this.include; + } + + public void setInclude(Set 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, + + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/WebRequestTraceFilter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/WebRequestTraceFilter.java index df85cd81a37..51a645891e3 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/WebRequestTraceFilter.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/WebRequestTraceFilter.java @@ -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 trace = getTrace(request); - if (this.logger.isTraceEnabled()) { - this.logger.trace("Processing request " + request.getMethod() + " " - + request.getRequestURI()); - if (this.dumpRequests) { - @SuppressWarnings("unchecked") - Map headers = (Map) 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 trace, HttpServletResponse response) { - Map headers = new LinkedHashMap(); - for (String header : response.getHeaderNames()) { - String value = response.getHeader(header); - headers.put(header, value); - } - headers.put("status", "" + response.getStatus()); - @SuppressWarnings("unchecked") - Map allHeaders = (Map) trace.get("headers"); - allHeaders.put("response", headers); - } - protected Map getTrace(HttpServletRequest request) { + HttpSession session = request.getSession(false); + Throwable exception = (Throwable) request + .getAttribute("javax.servlet.error.exception"); + Principal userPrincipal = request.getUserPrincipal(); + Map trace = new LinkedHashMap(); + Map headers = new LinkedHashMap(); + 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 getRequestHeaders(HttpServletRequest request) { Map headers = new LinkedHashMap(); Enumeration names = request.getHeaderNames(); - while (names.hasMoreElements()) { String name = names.nextElement(); List 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 trace, HttpServletResponse response) { + Map headers = (Map) trace.get("headers"); + headers.put("response", getResponseHeaders(response)); + } + + private Map getResponseHeaders(HttpServletResponse response) { + Map headers = new LinkedHashMap(); + for (String header : response.getHeaderNames()) { + String value = response.getHeader(header); + headers.put(header, value); } - Map trace = new LinkedHashMap(); - Map allHeaders = new LinkedHashMap(); - 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 error = this.errorAttributes - .getErrorAttributes(requestAttributes, true); - trace.put("error", error); + headers.put("status", "" + response.getStatus()); + return headers; + } + + private void logTrace(HttpServletRequest request, Map 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 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) { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/WebRequestTraceFilterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/WebRequestTraceFilterTests.java index d2fa44cfcad..54acfcdf532 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/WebRequestTraceFilterTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/WebRequestTraceFilterTests.java @@ -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 trace = this.filter.getTrace(request); assertEquals("GET", trace.get("method")); assertEquals("/foo", trace.get("path")); - @SuppressWarnings("unchecked") Map map = (Map) 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 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 trace = this.repository.findAll().iterator().next().getInfo(); Map map = (Map) 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 diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index a890d537002..6311ebcd20c 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -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 diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties index ccadbece711..b0d6a082138 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties @@ -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 +