diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/trace.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/trace.adoc new file mode 100644 index 0000000000..d1557376f8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/trace.adoc @@ -0,0 +1,29 @@ +[[trace]] += Trace (`trace`) + +The `trace` endpoint provides information about HTTP request-response exchanges. + + + +[[trace-retrieving]] +== Retrieving the Traces + +To retrieve the traces, make a `GET` request to `/actuator/trace`, as shown in the +following curl-based example: + +include::{snippets}trace/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}trace/http-response.adoc[] + + + +[[trace-retrieving-response-structure]] +=== Response Structure + +The response contains details of the traced HTTP request-response exchanges. The +following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}trace/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/index.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/index.adoc index c0d6a3ac04..7bab54f288 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/index.adoc +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/index.adoc @@ -68,3 +68,4 @@ include::endpoints/scheduledtasks.adoc[leveloffset=+1] include::endpoints/sessions.adoc[leveloffset=+1] include::endpoints/shutdown.adoc[leveloffset=+1] include::endpoints/threaddump.adoc[leveloffset=+1] +include::endpoints/trace.adoc[leveloffset=+1] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/TraceRepositoryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/TraceRepositoryAutoConfiguration.java deleted file mode 100644 index bd1a4859b0..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/TraceRepositoryAutoConfiguration.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2017 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.autoconfigure.trace; - -import org.springframework.boot.actuate.trace.InMemoryTraceRepository; -import org.springframework.boot.actuate.trace.TraceRepository; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for {@link TraceRepository tracing}. - * - * @author Dave Syer - * @since 2.0.0 - */ -@Configuration -public class TraceRepositoryAutoConfiguration { - - @ConditionalOnMissingBean(TraceRepository.class) - @Bean - public InMemoryTraceRepository traceRepository() { - return new InMemoryTraceRepository(); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/TraceWebFilterAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/TraceWebFilterAutoConfiguration.java deleted file mode 100644 index b7674f9c80..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/TraceWebFilterAutoConfiguration.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2012-2017 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.autoconfigure.trace; - -import javax.servlet.Servlet; -import javax.servlet.ServletRegistration; - -import org.springframework.beans.factory.ObjectProvider; -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.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.web.servlet.error.ErrorAttributes; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.DispatcherServlet; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for {@link WebRequestTraceFilter - * tracing}. - * - * @author Dave Syer - * @since 2.0.0 - */ -@Configuration -@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, ServletRegistration.class }) -@AutoConfigureAfter(TraceRepositoryAutoConfiguration.class) -@ConditionalOnProperty(prefix = "management.trace.filter", name = "enabled", matchIfMissing = true) -@EnableConfigurationProperties(TraceEndpointProperties.class) -public class TraceWebFilterAutoConfiguration { - - private final TraceRepository traceRepository; - - private final TraceEndpointProperties endpointProperties; - - private final ErrorAttributes errorAttributes; - - public TraceWebFilterAutoConfiguration(TraceRepository traceRepository, - TraceEndpointProperties endpointProperties, - ObjectProvider errorAttributes) { - this.traceRepository = traceRepository; - this.endpointProperties = endpointProperties; - this.errorAttributes = errorAttributes.getIfAvailable(); - } - - @Bean - @ConditionalOnMissingBean - public WebRequestTraceFilter webRequestLoggingFilter() { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.traceRepository, - this.endpointProperties.getInclude()); - if (this.errorAttributes != null) { - filter.setErrorAttributes(this.errorAttributes); - } - return filter; - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceAutoConfiguration.java new file mode 100644 index 0000000000..33f8b9042e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceAutoConfiguration.java @@ -0,0 +1,82 @@ +/* + * 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.actuate.autoconfigure.web.trace; + +import org.springframework.boot.actuate.web.trace.HttpExchangeTracer; +import org.springframework.boot.actuate.web.trace.HttpTraceRepository; +import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository; +import org.springframework.boot.actuate.web.trace.reactive.HttpTraceWebFilter; +import org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for HTTP tracing. + * + * @author Dave Syer + * @since 2.0.0 + */ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnProperty(prefix = "management.trace", name = "enabled", matchIfMissing = true) +@EnableConfigurationProperties(TraceProperties.class) +public class TraceAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(HttpTraceRepository.class) + public InMemoryHttpTraceRepository traceRepository() { + return new InMemoryHttpTraceRepository(); + } + + @Bean + @ConditionalOnMissingBean + public HttpExchangeTracer httpExchangeTracer(TraceProperties traceProperties) { + return new HttpExchangeTracer(traceProperties.getInclude()); + } + + @ConditionalOnWebApplication(type = Type.SERVLET) + static class ServletTraceFilterConfiguration { + + @Bean + @ConditionalOnMissingBean + public HttpTraceFilter httpTraceFilter(HttpTraceRepository repository, + HttpExchangeTracer tracer) { + return new HttpTraceFilter(repository, tracer); + } + + } + + @ConditionalOnWebApplication(type = Type.REACTIVE) + static class ReactiveTraceFilterConfiguration { + + @Bean + @ConditionalOnMissingBean + public HttpTraceWebFilter httpTraceWebFilter(HttpTraceRepository repository, + HttpExchangeTracer tracer, TraceProperties traceProperties) { + return new HttpTraceWebFilter(repository, tracer, + traceProperties.getInclude()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/TraceEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceEndpointAutoConfiguration.java similarity index 64% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/TraceEndpointAutoConfiguration.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceEndpointAutoConfiguration.java index aca48c5e79..fb95344c41 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/TraceEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -14,33 +14,34 @@ * limitations under the License. */ -package org.springframework.boot.actuate.autoconfigure.trace; +package org.springframework.boot.actuate.autoconfigure.web.trace; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; -import org.springframework.boot.actuate.trace.InMemoryTraceRepository; -import org.springframework.boot.actuate.trace.TraceEndpoint; -import org.springframework.boot.actuate.trace.TraceRepository; +import org.springframework.boot.actuate.web.trace.HttpTraceEndpoint; +import org.springframework.boot.actuate.web.trace.HttpTraceRepository; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** - * {@link EnableAutoConfiguration Auto-configuration} for the {@link TraceEndpoint}. + * {@link EnableAutoConfiguration Auto-configuration} for the {@link HttpTraceEndpoint}. * * @author Phillip Webb * @since 2.0.0 */ @Configuration +@AutoConfigureAfter(TraceAutoConfiguration.class) public class TraceEndpointAutoConfiguration { @Bean + @ConditionalOnBean(HttpTraceRepository.class) @ConditionalOnMissingBean @ConditionalOnEnabledEndpoint - public TraceEndpoint traceEndpoint(ObjectProvider traceRepository) { - return new TraceEndpoint( - traceRepository.getIfAvailable(() -> new InMemoryTraceRepository())); + public HttpTraceEndpoint traceEndpoint(HttpTraceRepository traceRepository) { + return new HttpTraceEndpoint(traceRepository); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/TraceEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceProperties.java similarity index 73% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/TraceEndpointProperties.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceProperties.java index a3138a1c69..8815a52211 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/TraceEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceProperties.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.boot.actuate.autoconfigure.trace; +package org.springframework.boot.actuate.autoconfigure.web.trace; import java.util.HashSet; import java.util.Set; -import org.springframework.boot.actuate.trace.Include; +import org.springframework.boot.actuate.web.trace.Include; import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -30,14 +30,15 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * @author Venil Noronha * @author Madhura Bhave * @author Stephane Nicoll - * @since 1.3.0 + * @since 2.0.0 */ @ConfigurationProperties(prefix = "management.trace") -public class TraceEndpointProperties { +public class TraceProperties { /** - * Items to be included in the trace. Defaults to request/response headers (including - * cookies) and errors. + * Items to be included in the trace. Defaults to request headers (excluding + * Authorization but including Cookie), response headers (including Set-Cookie), and + * time taken. */ private Set include = new HashSet<>(Include.defaultIncludes()); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/trace/package-info.java similarity index 83% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/package-info.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/trace/package-info.java index 0bec580d50..8c188509a7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/package-info.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/trace/package-info.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -17,4 +17,4 @@ /** * Auto-configuration for actuator tracing concerns. */ -package org.springframework.boot.actuate.autoconfigure.trace; +package org.springframework.boot.actuate.autoconfigure.web.trace; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index ee46ffeb56..2d8fcc3d89 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -185,12 +185,6 @@ "name": "management.info.git.mode", "defaultValue": "simple" }, - { - "name": "management.trace.filter.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable the trace servlet filter.", - "defaultValue": true - }, { "name": "management.metrics.binders.jvm.enabled", "type": "java.lang.Boolean", @@ -239,6 +233,21 @@ "description": "Instrument all available connection factories.", "defaultValue": true }, + { + "name": "management.trace.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable HTTP request-response tracing.", + "defaultValue": true + }, + { + "name": "management.trace.filter.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable the trace servlet filter.", + "deprecation": { + "replacement": "management.trace.enabled", + "level": "error" + } + }, { "name": "endpoints.actuator.enabled", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index 43a4e37b4c..a2ec9dcfd8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -39,13 +39,12 @@ org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpoint org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.solr.SolrHealthIndicatorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorAutoConfiguration,\ -org.springframework.boot.actuate.autoconfigure.trace.TraceEndpointAutoConfiguration,\ -org.springframework.boot.actuate.autoconfigure.trace.TraceRepositoryAutoConfiguration,\ -org.springframework.boot.actuate.autoconfigure.trace.TraceWebFilterAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementContextAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration,\ -org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration +org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.web.trace.TraceAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.web.trace.TraceEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration=\ org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HttpTraceEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HttpTraceEndpointDocumentationTests.java new file mode 100644 index 0000000000..8df90a9568 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HttpTraceEndpointDocumentationTests.java @@ -0,0 +1,130 @@ +/* + * 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.actuate.autoconfigure.endpoint.web.documentation; + +import java.net.URI; +import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.UUID; + +import org.junit.Test; + +import org.springframework.boot.actuate.web.trace.HttpExchangeTracer; +import org.springframework.boot.actuate.web.trace.HttpTrace; +import org.springframework.boot.actuate.web.trace.HttpTraceEndpoint; +import org.springframework.boot.actuate.web.trace.HttpTraceRepository; +import org.springframework.boot.actuate.web.trace.Include; +import org.springframework.boot.actuate.web.trace.TraceableRequest; +import org.springframework.boot.actuate.web.trace.TraceableResponse; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.payload.JsonFieldType; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for generating documentation describing {@link HttpTraceEndpoint}. + * + * @author Andy Wilkinson + */ +public class HttpTraceEndpointDocumentationTests + extends MockMvcEndpointDocumentationTests { + + @MockBean + private HttpTraceRepository repository; + + @Test + public void traces() throws Exception { + TraceableRequest request = mock(TraceableRequest.class); + given(request.getUri()).willReturn(URI.create("https://api.example.com")); + given(request.getMethod()).willReturn("GET"); + given(request.getHeaders()).willReturn(Collections + .singletonMap(HttpHeaders.ACCEPT, Arrays.asList("application/json"))); + TraceableResponse response = mock(TraceableResponse.class); + given(response.getStatus()).willReturn(200); + given(response.getHeaders()).willReturn(Collections.singletonMap( + HttpHeaders.CONTENT_TYPE, Arrays.asList("application/json"))); + Principal principal = mock(Principal.class); + given(principal.getName()).willReturn("alice"); + HttpExchangeTracer tracer = new HttpExchangeTracer(EnumSet.allOf(Include.class)); + HttpTrace trace = tracer.receivedRequest(request); + tracer.sendingResponse(trace, response, () -> principal, + () -> UUID.randomUUID().toString()); + given(this.repository.findAll()).willReturn(Arrays.asList(trace)); + this.mockMvc.perform(get("/actuator/trace")).andExpect(status().isOk()) + .andDo(document("trace", + responseFields( + fieldWithPath("traces").description( + "An array of traced HTTP request-response exchanges."), + fieldWithPath("traces.[].timestamp").description( + "Timestamp of when the traced exchange occurred."), + fieldWithPath("traces.[].principal") + .description("Principal of the exchange, if any.") + .optional(), + fieldWithPath("traces.[].principal.name") + .description("Name of the principal.").optional(), + fieldWithPath("traces.[].request.method") + .description("HTTP method of the request."), + fieldWithPath("traces.[].request.remoteAddress") + .description( + "Remote address from which the request was received, if known.") + .optional().type(JsonFieldType.STRING), + fieldWithPath("traces.[].request.uri") + .description("URI of the request."), + fieldWithPath("traces.[].request.headers").description( + "Headers of the request, keyed by header name."), + fieldWithPath("traces.[].request.headers.*.[]") + .description("Values of the header"), + fieldWithPath("traces.[].response.status") + .description("Status of the response"), + fieldWithPath("traces.[].response.headers").description( + "Headers of the response, keyed by header name."), + fieldWithPath("traces.[].response.headers.*.[]") + .description("Values of the header"), + fieldWithPath("traces.[].session") + .description( + "Session associated with the exchange, if any.") + .optional(), + fieldWithPath("traces.[].session.id") + .description("ID of the session."), + fieldWithPath("traces.[].timeTaken").description( + "Time, in milliseconds, taken to handle the exchange.")))); + } + + @Configuration + @Import(BaseDocumentationConfiguration.class) + static class TestConfiguration { + + @Bean + public HttpTraceEndpoint httpTraceEndpoint(HttpTraceRepository repository) { + return new HttpTraceEndpoint(repository); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/EndpointAutoConfigurationClasses.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/EndpointAutoConfigurationClasses.java index 402d8ee567..c28e50389b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/EndpointAutoConfigurationClasses.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/EndpointAutoConfigurationClasses.java @@ -28,8 +28,8 @@ import org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAut import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.trace.TraceEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.trace.TraceEndpointAutoConfiguration; /** * A list of all endpoint auto-configuration classes for use in tests. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JmxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JmxEndpointIntegrationTests.java index 9ddcc39f2a..1b1c8d866e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JmxEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JmxEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -28,9 +28,10 @@ import org.junit.Test; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.trace.TraceAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -43,9 +44,10 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class JmxEndpointIntegrationTests { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(JmxAutoConfiguration.class, - EndpointAutoConfiguration.class, JmxEndpointAutoConfiguration.class)) + EndpointAutoConfiguration.class, JmxEndpointAutoConfiguration.class, + TraceAutoConfiguration.class)) .withConfiguration( AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointExposureIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointExposureIntegrationTests.java index f5176f4d7a..63b17b9e0b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointExposureIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointExposureIntegrationTests.java @@ -22,6 +22,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfi import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.trace.TraceAutoConfiguration; import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; @@ -59,7 +60,8 @@ public class WebMvcEndpointExposureIntegrationTests { ManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, ManagementContextAutoConfiguration.class, - ServletManagementContextAutoConfiguration.class)) + ServletManagementContextAutoConfiguration.class, + TraceAutoConfiguration.class)) .withConfiguration( AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL)) .withUserConfiguration(CustomMvcEndpoint.class); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/trace/TraceRepositoryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/trace/TraceRepositoryAutoConfigurationTests.java deleted file mode 100644 index 5d7b507391..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/trace/TraceRepositoryAutoConfigurationTests.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2012-2017 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.autoconfigure.trace; - -import org.junit.Test; - -import org.springframework.boot.actuate.trace.InMemoryTraceRepository; -import org.springframework.boot.actuate.trace.TraceRepository; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link TraceRepositoryAutoConfiguration}. - * - * @author Phillip Webb - */ -public class TraceRepositoryAutoConfigurationTests { - - @Test - public void configuresInMemoryTraceRepository() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - TraceRepositoryAutoConfiguration.class); - assertThat(context.getBean(InMemoryTraceRepository.class)).isNotNull(); - context.close(); - } - - @Test - public void skipsIfRepositoryExists() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - Config.class, TraceRepositoryAutoConfiguration.class); - assertThat(context.getBeansOfType(InMemoryTraceRepository.class)).isEmpty(); - assertThat(context.getBeansOfType(TraceRepository.class)).hasSize(1); - context.close(); - } - - @Configuration - public static class Config { - - @Bean - public TraceRepository traceRepository() { - return mock(TraceRepository.class); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/trace/TraceWebFilterAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/trace/TraceWebFilterAutoConfigurationTests.java deleted file mode 100644 index fe398b86aa..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/trace/TraceWebFilterAutoConfigurationTests.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2012-2017 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.autoconfigure.trace; - -import java.util.Map; - -import org.junit.After; -import org.junit.Test; - -import org.springframework.boot.actuate.trace.TraceRepository; -import org.springframework.boot.actuate.trace.WebRequestTraceFilter; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link TraceWebFilterAutoConfiguration}. - * - * @author Phillip Webb - * @author Stephane Nicoll - */ -public class TraceWebFilterAutoConfigurationTests { - - private AnnotationConfigApplicationContext context; - - @After - public void close() { - if (this.context != null) { - this.context.close(); - } - } - - @Test - public void configureFilter() { - load(); - assertThat(this.context.getBean(WebRequestTraceFilter.class)).isNotNull(); - } - - @Test - public void overrideTraceFilter() { - load(CustomTraceFilterConfig.class); - WebRequestTraceFilter filter = this.context.getBean(WebRequestTraceFilter.class); - assertThat(filter).isInstanceOf(TestWebRequestTraceFilter.class); - } - - @Test - public void skipsFilterIfPropertyDisabled() { - load("management.trace.filter.enabled:false"); - assertThat(this.context.getBeansOfType(WebRequestTraceFilter.class).size()) - .isEqualTo(0); - } - - private void load(String... environment) { - load(null, environment); - } - - private void load(Class config, String... environment) { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of(environment).applyTo(context); - if (config != null) { - context.register(config); - } - context.register(PropertyPlaceholderAutoConfiguration.class, - TraceRepositoryAutoConfiguration.class, - TraceWebFilterAutoConfiguration.class); - context.refresh(); - this.context = context; - } - - @Configuration - static class CustomTraceFilterConfig { - - @Bean - public TestWebRequestTraceFilter testWebRequestTraceFilter( - TraceRepository repository, TraceEndpointProperties properties) { - return new TestWebRequestTraceFilter(repository, properties); - } - - } - - static class TestWebRequestTraceFilter extends WebRequestTraceFilter { - - TestWebRequestTraceFilter(TraceRepository repository, - TraceEndpointProperties properties) { - super(repository, properties.getInclude()); - } - - @Override - protected void postProcessRequestHeaders(Map headers) { - headers.clear(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceAutoConfigurationTests.java new file mode 100644 index 0000000000..faab962e68 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceAutoConfigurationTests.java @@ -0,0 +1,218 @@ +/* + * 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.actuate.autoconfigure.web.trace; + +import java.util.List; +import java.util.Set; + +import org.junit.Test; + +import org.springframework.boot.actuate.web.trace.HttpExchangeTracer; +import org.springframework.boot.actuate.web.trace.HttpTrace; +import org.springframework.boot.actuate.web.trace.HttpTraceRepository; +import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository; +import org.springframework.boot.actuate.web.trace.Include; +import org.springframework.boot.actuate.web.trace.reactive.HttpTraceWebFilter; +import org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TraceAutoConfiguration}. + * + * @author Andy Wilkinson + */ +public class TraceAutoConfigurationTests { + + @Test + public void configuresRepository() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class)) + .run((context) -> assertThat(context) + .hasSingleBean(InMemoryHttpTraceRepository.class)); + } + + @Test + public void usesUserProvidedRepository() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class)) + .withUserConfiguration(CustomRepositoryConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(HttpTraceRepository.class); + assertThat(context.getBean(HttpTraceRepository.class)) + .isInstanceOf(CustomHttpTraceRepository.class); + }); + } + + @Test + public void configuresTracer() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class)) + .run((context) -> assertThat(context) + .hasSingleBean(HttpExchangeTracer.class)); + } + + @Test + public void usesUserProvidedTracer() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class)) + .withUserConfiguration(CustomTracerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(HttpExchangeTracer.class); + assertThat(context.getBean(HttpExchangeTracer.class)) + .isInstanceOf(CustomHttpExchangeTracer.class); + }); + } + + @Test + public void configuresWebFilter() { + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class)) + .run((context) -> assertThat(context) + .hasSingleBean(HttpTraceWebFilter.class)); + } + + @Test + public void usesUserProvidedWebFilter() { + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class)) + .withUserConfiguration(CustomWebFilterConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(HttpTraceWebFilter.class); + assertThat(context.getBean(HttpTraceWebFilter.class)) + .isInstanceOf(CustomHttpTraceWebFilter.class); + }); + } + + @Test + public void configuresServletFilter() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class)) + .run((context) -> assertThat(context) + .hasSingleBean(HttpTraceFilter.class)); + } + + @Test + public void usesUserProvidedServletFilter() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class)) + .withUserConfiguration(CustomFilterConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(HttpTraceFilter.class); + assertThat(context.getBean(HttpTraceFilter.class)) + .isInstanceOf(CustomHttpTraceFilter.class); + }); + } + + @Test + public void backsOffWhenDisabled() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class)) + .withPropertyValues("management.trace.enabled=false") + .run((context) -> assertThat(context) + .doesNotHaveBean(InMemoryHttpTraceRepository.class) + .doesNotHaveBean(HttpExchangeTracer.class) + .doesNotHaveBean(HttpTraceFilter.class)); + } + + private static class CustomHttpTraceRepository implements HttpTraceRepository { + + @Override + public List findAll() { + return null; + } + + @Override + public void add(HttpTrace trace) { + + } + + } + + @Configuration + static class CustomRepositoryConfiguration { + + @Bean + public CustomHttpTraceRepository customRepository() { + return new CustomHttpTraceRepository(); + } + + } + + private static final class CustomHttpExchangeTracer extends HttpExchangeTracer { + + private CustomHttpExchangeTracer(Set includes) { + super(includes); + } + + } + + @Configuration + static class CustomTracerConfiguration { + + @Bean + public CustomHttpExchangeTracer customTracer(TraceProperties properties) { + return new CustomHttpExchangeTracer(properties.getInclude()); + } + + } + + private static final class CustomHttpTraceWebFilter extends HttpTraceWebFilter { + + private CustomHttpTraceWebFilter(HttpTraceRepository repository, + HttpExchangeTracer tracer, Set includes) { + super(repository, tracer, includes); + } + + } + + @Configuration + static class CustomWebFilterConfiguration { + + @Bean + public CustomHttpTraceWebFilter customWebFilter(HttpTraceRepository repository, + HttpExchangeTracer tracer, TraceProperties properties) { + return new CustomHttpTraceWebFilter(repository, tracer, + properties.getInclude()); + } + + } + + private static final class CustomHttpTraceFilter extends HttpTraceFilter { + + private CustomHttpTraceFilter(HttpTraceRepository repository, + HttpExchangeTracer tracer) { + super(repository, tracer); + } + + } + + @Configuration + static class CustomFilterConfiguration { + + @Bean + public CustomHttpTraceFilter customWebFilter(HttpTraceRepository repository, + HttpExchangeTracer tracer) { + return new CustomHttpTraceFilter(repository, tracer); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/trace/TraceEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceEndpointAutoConfigurationTests.java similarity index 56% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/trace/TraceEndpointAutoConfigurationTests.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceEndpointAutoConfigurationTests.java index 2b8619ef6e..28dd595dda 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/trace/TraceEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/TraceEndpointAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.boot.actuate.autoconfigure.trace; +package org.springframework.boot.actuate.autoconfigure.web.trace; import org.junit.Test; -import org.springframework.boot.actuate.trace.TraceEndpoint; +import org.springframework.boot.actuate.web.trace.HttpTraceEndpoint; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -31,21 +31,28 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class TraceEndpointAutoConfigurationTests { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of(TraceEndpointAutoConfiguration.class)); + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class, + TraceEndpointAutoConfiguration.class)); @Test public void runShouldHaveEndpointBean() { - this.contextRunner - .run((context) -> assertThat(context).hasSingleBean(TraceEndpoint.class)); + this.contextRunner.run( + (context) -> assertThat(context).hasSingleBean(HttpTraceEndpoint.class)); } @Test public void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() { this.contextRunner.withPropertyValues("management.endpoint.trace.enabled:false") .run((context) -> assertThat(context) - .doesNotHaveBean(TraceEndpoint.class)); + .doesNotHaveBean(HttpTraceEndpoint.class)); + } + + @Test + public void endpointBacksOffWhenRepositoryIsNotAvailable() { + this.contextRunner.withPropertyValues("management.trace.enabled:false") + .run((context) -> assertThat(context) + .doesNotHaveBean(HttpTraceEndpoint.class)); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/Trace.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/Trace.java deleted file mode 100644 index d3ab58201b..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/Trace.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.actuate.trace; - -import java.time.Instant; -import java.util.Map; - -import org.springframework.util.Assert; - -/** - * A value object representing a trace event: at a particular time with a simple (map) - * information. Can be used for analyzing contextual information such as HTTP headers. - * - * @author Dave Syer - */ -public final class Trace { - - private final Instant timestamp; - - private final Map info; - - public Trace(Instant timestamp, Map info) { - Assert.notNull(timestamp, "Timestamp must not be null"); - Assert.notNull(info, "Info must not be null"); - this.timestamp = timestamp; - this.info = info; - } - - public Instant getTimestamp() { - return this.timestamp; - } - - public Map getInfo() { - return this.info; - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/WebRequestTraceFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/WebRequestTraceFilter.java deleted file mode 100644 index 65e9ea555b..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/WebRequestTraceFilter.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright 2012-2017 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.io.IOException; -import java.security.Principal; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; -import javax.servlet.http.HttpSession; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.web.servlet.error.ErrorAttributes; -import org.springframework.core.Ordered; -import org.springframework.http.HttpStatus; -import org.springframework.web.context.request.ServletWebRequest; -import org.springframework.web.filter.OncePerRequestFilter; - -/** - * Servlet {@link Filter} that logs all requests to a {@link TraceRepository}. - * - * @author Dave Syer - * @author Wallace Wadge - * @author Andy Wilkinson - * @author Venil Noronha - * @author Madhura Bhave - */ -public class WebRequestTraceFilter extends OncePerRequestFilter implements Ordered { - - private static final Log logger = LogFactory.getLog(WebRequestTraceFilter.class); - - private boolean dumpRequests = false; - - // Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all - // enriched headers, but users can add stuff after this if they want to - private int order = Ordered.LOWEST_PRECEDENCE - 10; - - private final TraceRepository repository; - - private ErrorAttributes errorAttributes; - - private final Set includes; - - /** - * Create a new {@link WebRequestTraceFilter} instance. - * @param repository the trace repository - * @param includes the {@link Include} to apply - */ - public WebRequestTraceFilter(TraceRepository repository, Set includes) { - this.repository = repository; - this.includes = includes; - } - - /** - * Create a new {@link WebRequestTraceFilter} instance with the default - * {@link Include} to apply. - * @param repository the trace repository - * @see Include#defaultIncludes() - */ - public WebRequestTraceFilter(TraceRepository repository) { - this(repository, Include.defaultIncludes()); - } - - /** - * Debugging feature. If enabled, and trace logging is enabled then web request - * headers will be logged. - * @param dumpRequests if requests should be logged - */ - public void setDumpRequests(boolean dumpRequests) { - this.dumpRequests = dumpRequests; - } - - @Override - public int getOrder() { - return this.order; - } - - public void setOrder(int order) { - this.order = order; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - long startTime = System.nanoTime(); - Map trace = getTrace(request); - logTrace(request, trace); - int status = HttpStatus.INTERNAL_SERVER_ERROR.value(); - try { - filterChain.doFilter(request, response); - status = response.getStatus(); - } - finally { - addTimeTaken(trace, startTime); - enhanceTrace(trace, status == response.getStatus() ? response - : new CustomStatusResponseWrapper(response, status)); - this.repository.add(trace); - } - } - - 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())); - if (isIncluded(Include.PARAMETERS)) { - trace.put("parameters", getParameterMapCopy(request)); - } - 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 ServletWebRequest(request), true)); - } - return trace; - } - - private Map getRequestHeaders(HttpServletRequest request) { - Map headers = new LinkedHashMap<>(); - Set excludedHeaders = getExcludeHeaders(); - Enumeration names = request.getHeaderNames(); - while (names.hasMoreElements()) { - String name = names.nextElement(); - if (!excludedHeaders.contains(name.toLowerCase())) { - headers.put(name, getHeaderValue(request, name)); - } - } - postProcessRequestHeaders(headers); - return headers; - } - - private Set getExcludeHeaders() { - Set excludedHeaders = new HashSet<>(); - if (!isIncluded(Include.COOKIES)) { - excludedHeaders.add("cookie"); - } - if (!isIncluded(Include.AUTHORIZATION_HEADER)) { - excludedHeaders.add("authorization"); - } - return excludedHeaders; - } - - private Object getHeaderValue(HttpServletRequest request, String name) { - List value = Collections.list(request.getHeaders(name)); - if (value.size() == 1) { - return value.get(0); - } - if (value.isEmpty()) { - return ""; - } - return value; - } - - private Map getParameterMapCopy(HttpServletRequest request) { - return new LinkedHashMap<>(request.getParameterMap()); - } - - /** - * Post process request headers before they are added to the trace. - * @param headers a mutable map containing the request headers to trace - * @since 1.4.0 - */ - protected void postProcessRequestHeaders(Map headers) { - } - - private void addTimeTaken(Map trace, long startTime) { - long timeTaken = System.nanoTime() - startTime; - add(trace, Include.TIME_TAKEN, "timeTaken", - String.valueOf(TimeUnit.NANOSECONDS.toMillis(timeTaken))); - } - - @SuppressWarnings("unchecked") - protected void enhanceTrace(Map trace, HttpServletResponse response) { - if (isIncluded(Include.RESPONSE_HEADERS)) { - 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); - } - if (!isIncluded(Include.COOKIES)) { - headers.remove("Set-Cookie"); - } - headers.put("status", String.valueOf(response.getStatus())); - return headers; - } - - private void logTrace(HttpServletRequest request, Map trace) { - if (logger.isTraceEnabled()) { - logger.trace("Processing request " + request.getMethod() + " " - + request.getRequestURI()); - if (this.dumpRequests) { - logger.trace("Headers: " + trace.get("headers")); - } - } - } - - 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.includes.contains(include); - } - - public void setErrorAttributes(ErrorAttributes errorAttributes) { - this.errorAttributes = errorAttributes; - } - - private static final class CustomStatusResponseWrapper - extends HttpServletResponseWrapper { - - private final int status; - - private CustomStatusResponseWrapper(HttpServletResponse response, int status) { - super(response); - this.status = status; - } - - @Override - public int getStatus() { - return this.status; - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/HttpExchangeTracer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/HttpExchangeTracer.java new file mode 100644 index 0000000000..7e24ae51ce --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/HttpExchangeTracer.java @@ -0,0 +1,183 @@ +/* + * 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.actuate.web.trace; + +import java.net.URI; +import java.security.Principal; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.springframework.http.HttpHeaders; + +/** + * Traces an HTTP request-response exchange. + * + * @author Andy Wilkinson + * @since 2.0.0 + */ +public class HttpExchangeTracer { + + private final Set includes; + + /** + * Creates a new {@code HttpExchangeTracer} that will use the given {@code includes} + * to determine the contents of its traces. + * @param includes the includes + */ + public HttpExchangeTracer(Set includes) { + this.includes = includes; + } + + /** + * Begins the tracing of the exchange that was initiated by the given {@code request} + * being received. + * @param request the received request + * @return the HTTP trace for the + */ + public final HttpTrace receivedRequest(TraceableRequest request) { + return new HttpTrace(new FilteredTraceableRequest(request)); + } + + /** + * Ends the tracing of the exchange that is being concluded by sending the given + * {@code response}. + * @param trace the trace for the exchange + * @param response the response the concludes the exchange + * @param principal a supplier for the exchange's principal + * @param sessionId a supplier for the id of the exchange's session + */ + public final void sendingResponse(HttpTrace trace, TraceableResponse response, + Supplier principal, Supplier sessionId) { + setIfIncluded(Include.TIME_TAKEN, + () -> System.currentTimeMillis() - trace.getTimestamp().toEpochMilli(), + trace::setTimeTaken); + setIfIncluded(Include.SESSION_ID, sessionId, trace::setSessionId); + setIfIncluded(Include.PRINCIPAL, principal, trace::setPrincipal); + trace.setResponse( + new HttpTrace.Response(new FilteredTraceableResponse(response))); + } + + /** + * Post-process the given mutable map of request {@code headers}. + * @param headers the headers to post-process + */ + protected void postProcessRequestHeaders(Map> headers) { + + } + + private T getIfIncluded(Include include, Supplier valueSupplier) { + return this.includes.contains(include) ? valueSupplier.get() : null; + } + + private void setIfIncluded(Include include, Supplier supplier, + Consumer consumer) { + if (this.includes.contains(include)) { + consumer.accept(supplier.get()); + } + } + + private Map> getHeadersIfIncluded(Include include, + Supplier>> headersSupplier, + Predicate headerPredicate) { + if (!this.includes.contains(include)) { + return new LinkedHashMap<>(); + } + Map> headers = headersSupplier.get(); + Iterator keys = headers.keySet().iterator(); + while (keys.hasNext()) { + if (!headerPredicate.test(keys.next())) { + keys.remove(); + } + } + return headers; + } + + private final class FilteredTraceableRequest implements TraceableRequest { + + private final TraceableRequest delegate; + + private FilteredTraceableRequest(TraceableRequest delegate) { + this.delegate = delegate; + } + + @Override + public String getMethod() { + return this.delegate.getMethod(); + } + + @Override + public URI getUri() { + return this.delegate.getUri(); + } + + @Override + public Map> getHeaders() { + return getHeadersIfIncluded(Include.REQUEST_HEADERS, + this.delegate::getHeaders, (name) -> { + if (name.equalsIgnoreCase(HttpHeaders.COOKIE)) { + return HttpExchangeTracer.this.includes + .contains(Include.COOKIE_HEADERS); + } + if (name.equalsIgnoreCase(HttpHeaders.AUTHORIZATION)) { + return HttpExchangeTracer.this.includes + .contains(Include.AUTHORIZATION_HEADER); + } + return true; + }); + } + + @Override + public String getRemoteAddress() { + return getIfIncluded(Include.REMOTE_ADDRESS, this.delegate::getRemoteAddress); + } + + } + + private final class FilteredTraceableResponse implements TraceableResponse { + + private final TraceableResponse delegate; + + private FilteredTraceableResponse(TraceableResponse delegate) { + this.delegate = delegate; + } + + @Override + public int getStatus() { + return this.delegate.getStatus(); + } + + @Override + public Map> getHeaders() { + return getHeadersIfIncluded(Include.RESPONSE_HEADERS, + this.delegate::getHeaders, (name) -> { + if (name.equalsIgnoreCase(HttpHeaders.SET_COOKIE)) { + return HttpExchangeTracer.this.includes + .contains(Include.COOKIE_HEADERS); + } + return true; + }); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/HttpTrace.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/HttpTrace.java new file mode 100644 index 0000000000..b1b0fb41e6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/HttpTrace.java @@ -0,0 +1,192 @@ +/* + * 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.actuate.web.trace; + +import java.net.URI; +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import org.springframework.util.StringUtils; + +/** + * A trace event for handling of an HTTP request and response exchange. Can be used for + * analyzing contextual information such as HTTP headers. + * + * @author Dave Syer + * @author Andy Wilkinson + * @since 2.0.0 + */ +public final class HttpTrace { + + private final Instant timestamp = Instant.now(); + + private volatile Principal principal; + + private volatile Session session; + + private final Request request; + + private volatile Response response; + + private volatile Long timeTaken; + + HttpTrace(TraceableRequest request) { + this.request = new Request(request); + } + + public Instant getTimestamp() { + return this.timestamp; + } + + void setPrincipal(java.security.Principal principal) { + if (principal != null) { + this.principal = new Principal(principal.getName()); + } + } + + public Principal getPrincipal() { + return this.principal; + } + + public Session getSession() { + return this.session; + } + + void setSessionId(String sessionId) { + if (StringUtils.hasText(sessionId)) { + this.session = new Session(sessionId); + } + } + + public Request getRequest() { + return this.request; + } + + public Response getResponse() { + return this.response; + } + + void setResponse(Response response) { + this.response = response; + } + + public Long getTimeTaken() { + return this.timeTaken; + } + + void setTimeTaken(long timeTaken) { + this.timeTaken = timeTaken; + } + + /** + * Trace of an HTTP request. + */ + public static final class Request { + + private final String method; + + private final URI uri; + + private final Map> headers; + + private final String remoteAddress; + + private Request(TraceableRequest request) { + this.method = request.getMethod(); + this.uri = request.getUri(); + this.headers = request.getHeaders(); + this.remoteAddress = request.getRemoteAddress(); + } + + public String getMethod() { + return this.method; + } + + public URI getUri() { + return this.uri; + } + + public Map> getHeaders() { + return this.headers; + } + + public String getRemoteAddress() { + return this.remoteAddress; + } + + } + + /** + * Trace of an HTTP response. + */ + public static final class Response { + + private final int status; + + private final Map> headers; + + Response(TraceableResponse response) { + this.status = response.getStatus(); + this.headers = response.getHeaders(); + } + + public int getStatus() { + return this.status; + } + + public Map> getHeaders() { + return this.headers; + } + + } + + /** + * Session associated with an HTTP request-response exchange. + */ + public static final class Session { + + private final String id; + + private Session(String id) { + this.id = id; + } + + public String getId() { + return this.id; + } + + } + + /** + * Principal associated with an HTTP request-response exchange. + */ + public static final class Principal { + + private final String name; + + private Principal(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/TraceEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/HttpTraceEndpoint.java similarity index 68% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/TraceEndpoint.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/HttpTraceEndpoint.java index 19c9e08f1f..92c96c557c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/TraceEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/HttpTraceEndpoint.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.actuate.trace; +package org.springframework.boot.actuate.web.trace; import java.util.List; @@ -23,21 +23,21 @@ import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.util.Assert; /** - * {@link Endpoint} to expose {@link Trace} information. + * {@link Endpoint} to expose {@link HttpTrace} information. * * @author Dave Syer * @since 2.0.0 */ @Endpoint(id = "trace") -public class TraceEndpoint { +public class HttpTraceEndpoint { - private final TraceRepository repository; + private final HttpTraceRepository repository; /** - * Create a new {@link TraceEndpoint} instance. + * Create a new {@link HttpTraceEndpoint} instance. * @param repository the trace repository */ - public TraceEndpoint(TraceRepository repository) { + public HttpTraceEndpoint(HttpTraceRepository repository) { Assert.notNull(repository, "Repository must not be null"); this.repository = repository; } @@ -48,18 +48,18 @@ public class TraceEndpoint { } /** - * A description of an application's {@link Trace} entries. Primarily intended for + * A description of an application's {@link HttpTrace} entries. Primarily intended for * serialization to JSON. */ public static final class TraceDescriptor { - private final List traces; + private final List traces; - private TraceDescriptor(List traces) { + private TraceDescriptor(List traces) { this.traces = traces; } - public List getTraces() { + public List getTraces() { return this.traces; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/TraceRepository.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/HttpTraceRepository.java similarity index 61% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/TraceRepository.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/HttpTraceRepository.java index 88949b4c8a..d352d3ec6c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/TraceRepository.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/HttpTraceRepository.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -14,28 +14,29 @@ * limitations under the License. */ -package org.springframework.boot.actuate.trace; +package org.springframework.boot.actuate.web.trace; import java.util.List; -import java.util.Map; /** - * A repository for {@link Trace}s. + * A repository for {@link HttpTrace}s. * * @author Dave Syer + * @author Andy Wilkinson + * @since 2.0.0 */ -public interface TraceRepository { +public interface HttpTraceRepository { /** - * Find all {@link Trace} objects contained in the repository. + * Find all {@link HttpTrace} objects contained in the repository. * @return the results */ - List findAll(); + List findAll(); /** - * Add a new {@link Trace} object at the current time. - * @param traceInfo trace information + * Adds a trace to the repository. + * @param trace the trace to add */ - void add(Map traceInfo); + void add(HttpTrace trace); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/InMemoryTraceRepository.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/InMemoryHttpTraceRepository.java similarity index 80% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/InMemoryTraceRepository.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/InMemoryHttpTraceRepository.java index cb457053e0..bceb311519 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/InMemoryTraceRepository.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/InMemoryHttpTraceRepository.java @@ -14,28 +14,27 @@ * limitations under the License. */ -package org.springframework.boot.actuate.trace; +package org.springframework.boot.actuate.web.trace; -import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.Map; /** - * In-memory implementation of {@link TraceRepository}. + * In-memory implementation of {@link HttpTraceRepository}. * * @author Dave Syer * @author Olivier Bourgain + * @since 2.0.0 */ -public class InMemoryTraceRepository implements TraceRepository { +public class InMemoryHttpTraceRepository implements HttpTraceRepository { private int capacity = 100; private boolean reverse = true; - private final List traces = new LinkedList<>(); + private final List traces = new LinkedList<>(); /** * Flag to say that the repository lists traces in reverse order. @@ -58,15 +57,14 @@ public class InMemoryTraceRepository implements TraceRepository { } @Override - public List findAll() { + public List findAll() { synchronized (this.traces) { return Collections.unmodifiableList(new ArrayList<>(this.traces)); } } @Override - public void add(Map map) { - Trace trace = new Trace(Instant.now(), map); + public void add(HttpTrace trace) { synchronized (this.traces) { while (this.traces.size() >= this.capacity) { this.traces.remove(this.reverse ? this.capacity - 1 : 0); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/Include.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/Include.java similarity index 67% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/Include.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/Include.java index 29f3f096dc..a877cd0908 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/Include.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/Include.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.actuate.trace; +package org.springframework.boot.actuate.web.trace; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; /** - * Include options for tracing. + * Include options for HTTP tracing. * * @author Wallace Wadge * @since 2.0.0 @@ -39,9 +39,10 @@ public enum Include { RESPONSE_HEADERS, /** - * Include "Cookie" in request and "Set-Cookie" in response headers. + * Include "Cookie" header (if any) in request headers and "Set-Cookie" (if any) in + * response headers. */ - COOKIES, + COOKIE_HEADERS, /** * Include authorization header (if any). @@ -49,44 +50,9 @@ public enum Include { AUTHORIZATION_HEADER, /** - * Include errors (if any). + * Include the principal. */ - 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, + PRINCIPAL, /** * Include the remote address. @@ -98,11 +64,6 @@ public enum Include { */ SESSION_ID, - /** - * Include the remote user. - */ - REMOTE_USER, - /** * Include the time taken to service the request in milliseconds. */ @@ -114,8 +75,7 @@ public enum Include { Set defaultIncludes = new LinkedHashSet<>(); defaultIncludes.add(Include.REQUEST_HEADERS); defaultIncludes.add(Include.RESPONSE_HEADERS); - defaultIncludes.add(Include.COOKIES); - defaultIncludes.add(Include.ERRORS); + defaultIncludes.add(Include.COOKIE_HEADERS); defaultIncludes.add(Include.TIME_TAKEN); DEFAULT_INCLUDES = Collections.unmodifiableSet(defaultIncludes); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/TraceableRequest.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/TraceableRequest.java new file mode 100644 index 0000000000..750cb4fd60 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/TraceableRequest.java @@ -0,0 +1,56 @@ +/* + * 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.actuate.web.trace; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +/** + * A representation of an HTTP request that is suitable for tracing. + * + * @author Andy Wilkinson + * @since 2.0.0 + * @see HttpExchangeTracer + */ +public interface TraceableRequest { + + /** + * Returns the method (GET, POST, etc) of the request. + * @return the method + */ + String getMethod(); + + /** + * Returns the URI of the request. + * @return the URI + */ + URI getUri(); + + /** + * Returns a modifiable copy of the headers of the request. + * @return the headers + */ + Map> getHeaders(); + + /** + * Returns the remote address from which the request was sent, if available. + * @return the remote address or {@code null} + */ + String getRemoteAddress(); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/TraceEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/TraceableResponse.java similarity index 50% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/TraceEndpointTests.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/TraceableResponse.java index f2250e8730..350465eeff 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/TraceEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/TraceableResponse.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -14,28 +14,30 @@ * limitations under the License. */ -package org.springframework.boot.actuate.trace; +package org.springframework.boot.actuate.web.trace; -import java.util.Collections; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; +import java.util.List; +import java.util.Map; /** - * Tests for {@link TraceEndpoint}. + * A representation of an HTTP response that is suitable for tracing. * - * @author Phillip Webb * @author Andy Wilkinson + * @since 2.0.0 + * @see HttpExchangeTracer */ -public class TraceEndpointTests { +public interface TraceableResponse { - @Test - public void trace() { - TraceRepository repository = new InMemoryTraceRepository(); - repository.add(Collections.singletonMap("a", "b")); - Trace trace = new TraceEndpoint(repository).traces().getTraces().get(0); - assertThat(trace.getInfo().get("a")).isEqualTo("b"); - } + /** + * The status of the response. + * @return the status + */ + int getStatus(); + + /** + * Returns a modifiable copy of the headers of the response. + * @return the headers + */ + Map> getHeaders(); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/package-info.java similarity index 73% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/package-info.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/package-info.java index 0b246ce691..f05e25da64 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/package-info.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/package-info.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -15,8 +15,8 @@ */ /** - * Actuator tracing support. + * Actuator HTTP tracing support. * - * @see org.springframework.boot.actuate.trace.TraceRepository + * @see org.springframework.boot.actuate.web.trace.HttpTraceRepository */ -package org.springframework.boot.actuate.trace; +package org.springframework.boot.actuate.web.trace; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/HttpTraceWebFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/HttpTraceWebFilter.java new file mode 100644 index 0000000000..c7523a3dba --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/HttpTraceWebFilter.java @@ -0,0 +1,132 @@ +/* + * 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.actuate.web.trace.reactive; + +import java.security.Principal; +import java.util.Set; + +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.web.trace.HttpExchangeTracer; +import org.springframework.boot.actuate.web.trace.HttpTrace; +import org.springframework.boot.actuate.web.trace.HttpTraceRepository; +import org.springframework.boot.actuate.web.trace.Include; +import org.springframework.core.Ordered; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpResponseDecorator; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.server.WebSession; + +/** + * A {@link WebFilter} for tracing HTTP requests. + * + * @author Andy Wilkinson + * @since 2.0.0 + */ +public class HttpTraceWebFilter implements WebFilter, Ordered { + + private static final Object NONE = new Object(); + + // Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all + // enriched headers, but users can add stuff after this if they want to + private int order = Ordered.LOWEST_PRECEDENCE - 10; + + private final HttpTraceRepository repository; + + private final HttpExchangeTracer tracer; + + private final Set includes; + + public HttpTraceWebFilter(HttpTraceRepository repository, HttpExchangeTracer tracer, + Set includes) { + this.repository = repository; + this.tracer = tracer; + this.includes = includes; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + Mono principal = this.includes.contains(Include.PRINCIPAL) + ? exchange.getPrincipal().cast(Object.class).defaultIfEmpty(NONE) + : Mono.just(NONE); + Mono session = this.includes.contains(Include.SESSION_ID) + ? exchange.getSession() : Mono.just(NONE); + return Mono.zip(principal, session) + .flatMap((tuple) -> filter(exchange, chain, + asType(tuple.getT1(), Principal.class), + asType(tuple.getT2(), WebSession.class))); + } + + private T asType(Object object, Class type) { + if (type.isInstance(object)) { + return type.cast(object); + } + return null; + } + + private Mono filter(ServerWebExchange exchange, WebFilterChain chain, + Principal principal, WebSession session) { + ServerWebExchangeTraceableRequest request = new ServerWebExchangeTraceableRequest( + exchange); + HttpTrace trace = this.tracer.receivedRequest(request); + return chain.filter(exchange).doAfterSuccessOrError((aVoid, ex) -> { + this.tracer.sendingResponse(trace, + new TraceableServerHttpResponse(ex == null ? exchange.getResponse() + : new CustomStatusResponseDecorator(ex, + exchange.getResponse())), + () -> principal, () -> getStartedSessionId(session)); + this.repository.add(trace); + }); + } + + private String getStartedSessionId(WebSession session) { + return (session != null && session.isStarted()) ? session.getId() : null; + } + + private static final class CustomStatusResponseDecorator + extends ServerHttpResponseDecorator { + + private final HttpStatus status; + + private CustomStatusResponseDecorator(Throwable ex, ServerHttpResponse delegate) { + super(delegate); + this.status = ex instanceof ResponseStatusException + ? ((ResponseStatusException) ex).getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + } + + @Override + public HttpStatus getStatusCode() { + return this.status; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequest.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequest.java new file mode 100644 index 0000000000..3e5b451824 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequest.java @@ -0,0 +1,72 @@ +/* + * 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.actuate.web.trace.reactive; + +import java.net.URI; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.actuate.web.trace.TraceableRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; + +/** + * A {@link TraceableRequest} backed by a {@link ServerWebExchange}. + * + * @author Andy Wilkinson + */ +class ServerWebExchangeTraceableRequest implements TraceableRequest { + + private final String method; + + private final Map> headers; + + private final URI uri; + + private final String remoteAddress; + + ServerWebExchangeTraceableRequest(ServerWebExchange exchange) { + ServerHttpRequest request = exchange.getRequest(); + this.method = request.getMethodValue(); + this.headers = request.getHeaders(); + this.uri = request.getURI(); + this.remoteAddress = request.getRemoteAddress() == null ? null + : request.getRemoteAddress().getAddress().toString(); + } + + @Override + public String getMethod() { + return this.method; + } + + @Override + public URI getUri() { + return this.uri; + } + + @Override + public Map> getHeaders() { + return new LinkedHashMap<>(this.headers); + } + + @Override + public String getRemoteAddress() { + return this.remoteAddress; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/TraceableServerHttpResponse.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/TraceableServerHttpResponse.java new file mode 100644 index 0000000000..8004f8afd7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/TraceableServerHttpResponse.java @@ -0,0 +1,50 @@ +/* + * 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.actuate.web.trace.reactive; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.actuate.web.trace.TraceableResponse; +import org.springframework.http.server.reactive.ServerHttpResponse; + +/** + * An adapter that exposes a {@link ServerHttpResponse} as a {@link TraceableResponse}. + * + * @author Andy Wilkinson + */ +class TraceableServerHttpResponse implements TraceableResponse { + + private final ServerHttpResponse response; + + TraceableServerHttpResponse(ServerHttpResponse exchange) { + this.response = exchange; + } + + @Override + public int getStatus() { + return this.response.getStatusCode() == null ? 200 + : this.response.getStatusCode().value(); + } + + @Override + public Map> getHeaders() { + return new LinkedHashMap<>(this.response.getHeaders()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/package-info.java new file mode 100644 index 0000000000..adbe4cb2ec --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** + * Actuator reactive HTTP tracing support. + * + * @see org.springframework.boot.actuate.web.trace.HttpTraceRepository + */ +package org.springframework.boot.actuate.web.trace.reactive; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/HttpTraceFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/HttpTraceFilter.java new file mode 100644 index 0000000000..45aa1d50b2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/HttpTraceFilter.java @@ -0,0 +1,119 @@ +/* + * 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.actuate.web.trace.servlet; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import javax.servlet.http.HttpSession; + +import org.springframework.boot.actuate.web.trace.HttpExchangeTracer; +import org.springframework.boot.actuate.web.trace.HttpTrace; +import org.springframework.boot.actuate.web.trace.HttpTraceRepository; +import org.springframework.core.Ordered; +import org.springframework.http.HttpStatus; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Servlet {@link Filter} that logs all requests to an {@link HttpTraceRepository}. + * + * @author Dave Syer + * @author Wallace Wadge + * @author Andy Wilkinson + * @author Venil Noronha + * @author Madhura Bhave + * @since 2.0.0 + */ +public class HttpTraceFilter extends OncePerRequestFilter implements Ordered { + + // Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all + // enriched headers, but users can add stuff after this if they want to + private int order = Ordered.LOWEST_PRECEDENCE - 10; + + private final HttpTraceRepository repository; + + private final HttpExchangeTracer tracer; + + /** + * Create a new {@link HttpTraceFilter} instance. + * @param repository the trace repository + * @param tracer used to trace exchanges + */ + public HttpTraceFilter(HttpTraceRepository repository, HttpExchangeTracer tracer) { + this.repository = repository; + this.tracer = tracer; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + TraceableHttpServletRequest traceableRequest = new TraceableHttpServletRequest( + request); + HttpTrace trace = this.tracer.receivedRequest(traceableRequest); + int status = HttpStatus.INTERNAL_SERVER_ERROR.value(); + try { + filterChain.doFilter(request, response); + status = response.getStatus(); + } + finally { + TraceableHttpServletResponse traceableResponse = new TraceableHttpServletResponse( + status == response.getStatus() ? response + : new CustomStatusResponseWrapper(response, status)); + this.tracer.sendingResponse(trace, traceableResponse, + request::getUserPrincipal, () -> getSessionId(request)); + this.repository.add(trace); + } + } + + private String getSessionId(HttpServletRequest request) { + HttpSession session = request.getSession(false); + return session == null ? null : session.getId(); + } + + private static final class CustomStatusResponseWrapper + extends HttpServletResponseWrapper { + + private final int status; + + private CustomStatusResponseWrapper(HttpServletResponse response, int status) { + super(response); + this.status = status; + } + + @Override + public int getStatus() { + return this.status; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletRequest.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletRequest.java new file mode 100644 index 0000000000..86af7488f5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletRequest.java @@ -0,0 +1,87 @@ +/* + * 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.actuate.web.trace.servlet; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.boot.actuate.web.trace.TraceableRequest; +import org.springframework.util.StringUtils; + +/** + * An adapter that exposes an {@link HttpServletRequest} as a {@link TraceableRequest}. + * + * @author Andy Wilkinson + */ +final class TraceableHttpServletRequest implements TraceableRequest { + + private final HttpServletRequest request; + + TraceableHttpServletRequest(HttpServletRequest request) { + this.request = request; + } + + @Override + public String getMethod() { + return this.request.getMethod(); + } + + @Override + public URI getUri() { + StringBuffer urlBuffer = this.request.getRequestURL(); + if (StringUtils.hasText(this.request.getQueryString())) { + urlBuffer.append("?"); + urlBuffer.append(this.request.getQueryString()); + } + return URI.create(urlBuffer.toString()); + } + + @Override + public Map> getHeaders() { + return extractHeaders(); + } + + @Override + public String getRemoteAddress() { + return this.request.getRemoteAddr(); + } + + private Map> extractHeaders() { + Map> headers = new LinkedHashMap<>(); + Enumeration names = this.request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + headers.put(name, toList(this.request.getHeaders(name))); + } + return headers; + } + + private List toList(Enumeration enumeration) { + List list = new ArrayList(); + while (enumeration.hasMoreElements()) { + list.add(enumeration.nextElement()); + } + return list; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletResponse.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletResponse.java new file mode 100644 index 0000000000..ee6d2ac251 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletResponse.java @@ -0,0 +1,59 @@ +/* + * 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.actuate.web.trace.servlet; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.boot.actuate.web.trace.TraceableResponse; + +/** + * An adapter that exposes an {@link HttpServletResponse} as a {@link TraceableResponse}. + * + * @author Andy Wilkinson + */ +final class TraceableHttpServletResponse implements TraceableResponse { + + private final HttpServletResponse delegate; + + TraceableHttpServletResponse(HttpServletResponse response) { + this.delegate = response; + } + + @Override + public int getStatus() { + return this.delegate.getStatus(); + } + + @Override + public Map> getHeaders() { + return extractHeaders(); + } + + private Map> extractHeaders() { + Map> headers = new LinkedHashMap<>(); + for (String name : this.delegate.getHeaderNames()) { + headers.put(name, new ArrayList<>(this.delegate.getHeaders(name))); + } + return headers; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/package-info.java new file mode 100644 index 0000000000..65c55e6919 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** + * Actuator servlet HTTP tracing support. + * + * @see org.springframework.boot.actuate.web.trace.HttpTraceRepository + */ +package org.springframework.boot.actuate.web.trace.servlet; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/InMemoryTraceRepositoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/InMemoryTraceRepositoryTests.java deleted file mode 100644 index 6980fde5b7..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/InMemoryTraceRepositoryTests.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2012-2017 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.List; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link InMemoryTraceRepository}. - * - * @author Dave Syer - */ -public class InMemoryTraceRepositoryTests { - - private final InMemoryTraceRepository repository = new InMemoryTraceRepository(); - - @Test - public void capacityLimited() { - this.repository.setCapacity(2); - this.repository.add(Collections.singletonMap("foo", "bar")); - this.repository.add(Collections.singletonMap("bar", "foo")); - this.repository.add(Collections.singletonMap("bar", "bar")); - List traces = this.repository.findAll(); - assertThat(traces).hasSize(2); - assertThat(traces.get(0).getInfo().get("bar")).isEqualTo("bar"); - assertThat(traces.get(1).getInfo().get("bar")).isEqualTo("foo"); - } - - @Test - public void reverseFalse() { - this.repository.setReverse(false); - this.repository.setCapacity(2); - this.repository.add(Collections.singletonMap("foo", "bar")); - this.repository.add(Collections.singletonMap("bar", "foo")); - this.repository.add(Collections.singletonMap("bar", "bar")); - List traces = this.repository.findAll(); - assertThat(traces).hasSize(2); - assertThat(traces.get(1).getInfo().get("bar")).isEqualTo("bar"); - assertThat(traces.get(0).getInfo().get("bar")).isEqualTo("foo"); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/WebRequestTraceFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/WebRequestTraceFilterTests.java deleted file mode 100644 index 8bc2f6657b..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/WebRequestTraceFilterTests.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright 2012-2017 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.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.security.Principal; -import java.util.Collections; -import java.util.EnumSet; -import java.util.Map; - -import javax.servlet.ServletException; - -import org.junit.Test; - -import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; -import org.springframework.mock.web.MockFilterChain; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link WebRequestTraceFilter}. - * - * @author Dave Syer - * @author Wallace Wadge - * @author Phillip Webb - * @author Andy Wilkinson - * @author Venil Noronha - * @author Stephane Nicoll - * @author Madhura Bhave - */ -public class WebRequestTraceFilterTests { - - private final InMemoryTraceRepository repository = new InMemoryTraceRepository(); - - @Test - @SuppressWarnings("unchecked") - public void filterAddsTraceWithDefaultIncludes() { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository); - MockHttpServletRequest request = spy(new MockHttpServletRequest("GET", "/foo")); - request.addHeader("Accept", "application/json"); - Map trace = filter.getTrace(request); - assertThat(trace.get("method")).isEqualTo("GET"); - assertThat(trace.get("path")).isEqualTo("/foo"); - Map map = (Map) trace.get("headers"); - assertThat(map.get("request").toString()).isEqualTo("{Accept=application/json}"); - verify(request, times(0)).getParameterMap(); - } - - @Test - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void filterAddsTraceWithCustomIncludes() throws IOException, ServletException { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository, - EnumSet.allOf(Include.class)); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addHeader("Accept", "application/json"); - request.addHeader("Cookie", "testCookie=testValue;"); - 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(); - request.setAuthType("authType"); - Principal principal = () -> "principalTest"; - request.setUserPrincipal(principal); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.addHeader("Content-Type", "application/json"); - response.addHeader("Set-Cookie", "a=b"); - filter.doFilterInternal(request, response, (req, resp) -> { - BufferedReader bufferedReader = req.getReader(); - while (bufferedReader.readLine() != null) { - // read the contents as normal (forces cache to fill up) - } - resp.getWriter().println("Goodbye, World!"); - }); - assertThat(this.repository.findAll()).hasSize(1); - Map trace = this.repository.findAll().iterator().next().getInfo(); - Map map = (Map) trace.get("headers"); - - assertThat(map.get("response").toString()) - .isEqualTo("{Content-Type=application/json, Set-Cookie=a=b, status=200}"); - assertThat(trace.get("method")).isEqualTo("GET"); - assertThat(trace.get("path")).isEqualTo("/foo"); - assertThat(((String[]) ((Map) trace.get("parameters")).get("param"))[0]) - .isEqualTo("paramvalue"); - assertThat(trace.get("remoteAddress")).isEqualTo("some.remote.addr"); - assertThat(trace.get("query")).isEqualTo("some.query.string"); - assertThat(trace.get("userPrincipal")).isEqualTo(principal.getName()); - assertThat(trace.get("contextPath")).isEqualTo("some.context.path"); - assertThat(trace.get("pathInfo")).isEqualTo(url); - assertThat(trace.get("authType")).isEqualTo("authType"); - assertThat(map.get("request").toString()) - .isEqualTo("{Accept=application/json, Cookie=testCookie=testValue;}"); - } - - @Test - @SuppressWarnings({ "unchecked" }) - public void filterDoesNotAddResponseHeadersWithoutResponseHeadersInclude() - throws ServletException, IOException { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository, - Collections.singleton(Include.REQUEST_HEADERS)); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.addHeader("Content-Type", "application/json"); - filter.doFilterInternal(request, response, (req, resp) -> { - }); - Map info = this.repository.findAll().iterator().next().getInfo(); - Map headers = (Map) info.get("headers"); - assertThat(headers.get("response") == null).isTrue(); - } - - @Test - @SuppressWarnings({ "unchecked" }) - public void filterDoesNotAddRequestCookiesWithCookiesExclude() { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository, - Collections.singleton(Include.REQUEST_HEADERS)); - MockHttpServletRequest request = spy(new MockHttpServletRequest("GET", "/foo")); - request.addHeader("Accept", "application/json"); - request.addHeader("Cookie", "testCookie=testValue;"); - Map map = (Map) filter.getTrace(request) - .get("headers"); - assertThat(map.get("request").toString()).isEqualTo("{Accept=application/json}"); - } - - @Test - @SuppressWarnings({ "unchecked" }) - public void filterDoesNotAddAuthorizationHeaderWithoutAuthorizationHeaderInclude() - throws ServletException, IOException { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addHeader("Authorization", "my-auth-header"); - MockHttpServletResponse response = new MockHttpServletResponse(); - filter.doFilterInternal(request, response, (req, resp) -> { - }); - Map info = this.repository.findAll().iterator().next().getInfo(); - Map headers = (Map) info.get("headers"); - assertThat(((Map) headers.get("request"))).hasSize(0); - } - - @Test - @SuppressWarnings({ "unchecked" }) - public void filterAddsAuthorizationHeaderWhenAuthorizationHeaderIncluded() - throws ServletException, IOException { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository, - EnumSet.of(Include.REQUEST_HEADERS, Include.AUTHORIZATION_HEADER)); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addHeader("Authorization", "my-auth-header"); - MockHttpServletResponse response = new MockHttpServletResponse(); - filter.doFilterInternal(request, response, (req, resp) -> { - }); - Map info = this.repository.findAll().iterator().next().getInfo(); - Map headers = (Map) info.get("headers"); - assertThat(((Map) headers.get("request"))) - .containsKey("Authorization"); - } - - @Test - @SuppressWarnings({ "unchecked" }) - public void filterDoesNotAddResponseCookiesWithCookiesExclude() { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository, - Collections.singleton(Include.RESPONSE_HEADERS)); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.addHeader("Content-Type", "application/json"); - response.addHeader("Set-Cookie", "testCookie=testValue;"); - Map trace = filter.getTrace(request); - filter.enhanceTrace(trace, response); - Map map = (Map) trace.get("headers"); - assertThat(map.get("response").toString()) - .isEqualTo("{Content-Type=application/json, status=200}"); - } - - @Test - public void filterHasResponseStatus() { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setStatus(404); - response.addHeader("Content-Type", "application/json"); - Map trace = filter.getTrace(request); - filter.enhanceTrace(trace, response); - @SuppressWarnings("unchecked") - Map map = (Map) ((Map) trace - .get("headers")).get("response"); - assertThat(map.get("status").toString()).isEqualTo("404"); - } - - @Test - public void filterAddsTimeTaken() throws Exception { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository); - MockHttpServletRequest request = spy(new MockHttpServletRequest("GET", "/foo")); - MockHttpServletResponse response = new MockHttpServletResponse(); - MockFilterChain chain = new MockFilterChain(); - filter.doFilter(request, response, chain); - String timeTaken = (String) this.repository.findAll().iterator().next().getInfo() - .get("timeTaken"); - assertThat(timeTaken).isNotNull(); - } - - @Test - public void filterHasError() { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository); - filter.setErrorAttributes(new DefaultErrorAttributes()); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setStatus(500); - request.setAttribute("javax.servlet.error.exception", - new IllegalStateException("Foo")); - response.addHeader("Content-Type", "application/json"); - Map trace = filter.getTrace(request); - filter.enhanceTrace(trace, response); - @SuppressWarnings("unchecked") - Map map = (Map) trace.get("error"); - System.err.println(map); - assertThat(map.get("message").toString()).isEqualTo("Foo"); - } - - @Test - @SuppressWarnings("unchecked") - public void filterHas500ResponseStatusWhenExceptionIsThrown() - throws ServletException, IOException { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - MockHttpServletResponse response = new MockHttpServletResponse(); - - try { - filter.doFilterInternal(request, response, (req, resp) -> { - throw new RuntimeException(); - }); - fail("Exception was swallowed"); - } - catch (RuntimeException ex) { - Map headers = (Map) this.repository.findAll() - .iterator().next().getInfo().get("headers"); - Map responseHeaders = (Map) headers - .get("response"); - assertThat((String) responseHeaders.get("status")).isEqualTo("500"); - } - } - - @Test - @SuppressWarnings("unchecked") - public void postProcessRequestHeaders() { - WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository) { - - @Override - protected void postProcessRequestHeaders(Map headers) { - headers.remove("Test"); - } - - }; - MockHttpServletRequest request = spy(new MockHttpServletRequest("GET", "/foo")); - request.addHeader("Accept", "application/json"); - request.addHeader("Test", "spring"); - Map map = (Map) filter.getTrace(request) - .get("headers"); - assertThat(map.get("request").toString()).isEqualTo("{Accept=application/json}"); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/HttpExchangeTracerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/HttpExchangeTracerTests.java new file mode 100644 index 0000000000..e50d6cd25d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/HttpExchangeTracerTests.java @@ -0,0 +1,335 @@ +/* + * 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.actuate.web.trace; + +import java.net.URI; +import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.boot.actuate.web.trace.HttpTrace.Request; +import org.springframework.http.HttpHeaders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HttpExchangeTracer}. + * + * @author Andy Wilkinson + */ +public class HttpExchangeTracerTests { + + @Test + public void methodIsIncluded() { + HttpTrace trace = new HttpExchangeTracer(EnumSet.noneOf(Include.class)) + .receivedRequest(createRequest()); + Request request = trace.getRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + } + + @Test + public void uriIsIncluded() { + HttpTrace trace = new HttpExchangeTracer(EnumSet.noneOf(Include.class)) + .receivedRequest(createRequest()); + Request request = trace.getRequest(); + assertThat(request.getUri()).isEqualTo(URI.create("https://api.example.com")); + } + + @Test + public void remoteAddressIsNotIncludedByDefault() { + HttpTrace trace = new HttpExchangeTracer(EnumSet.noneOf(Include.class)) + .receivedRequest(createRequest()); + Request request = trace.getRequest(); + assertThat(request.getRemoteAddress()).isNull(); + } + + @Test + public void remoteAddressCanBeIncluded() { + HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REMOTE_ADDRESS)) + .receivedRequest(createRequest()); + Request request = trace.getRequest(); + assertThat(request.getRemoteAddress()).isEqualTo("127.0.0.1"); + } + + @Test + public void requestHeadersAreNotIncludedByDefault() { + HttpTrace trace = new HttpExchangeTracer(EnumSet.noneOf(Include.class)) + .receivedRequest(createRequest()); + Request request = trace.getRequest(); + assertThat(request.getHeaders()).isEmpty(); + } + + @Test + public void requestHeadersCanBeIncluded() { + HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REQUEST_HEADERS)) + .receivedRequest(createRequest()); + Request request = trace.getRequest(); + assertThat(request.getHeaders()).containsOnlyKeys(HttpHeaders.ACCEPT); + } + + @Test + public void authorizationHeaderIsNotIncludedByDefault() { + HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REQUEST_HEADERS)) + .receivedRequest(createRequest(Collections.singletonMap( + HttpHeaders.AUTHORIZATION, Arrays.asList("secret")))); + Request request = trace.getRequest(); + assertThat(request.getHeaders()).isEmpty(); + } + + @Test + public void mixedCaseAuthorizationHeaderIsNotIncludedByDefault() { + HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REQUEST_HEADERS)) + .receivedRequest(createRequest(Collections.singletonMap( + mixedCase(HttpHeaders.AUTHORIZATION), Arrays.asList("secret")))); + Request request = trace.getRequest(); + assertThat(request.getHeaders()).isEmpty(); + } + + @Test + public void authorizationHeaderCanBeIncluded() { + HttpTrace trace = new HttpExchangeTracer( + EnumSet.of(Include.REQUEST_HEADERS, Include.AUTHORIZATION_HEADER)) + .receivedRequest(createRequest(Collections.singletonMap( + HttpHeaders.AUTHORIZATION, Arrays.asList("secret")))); + Request request = trace.getRequest(); + assertThat(request.getHeaders()).containsOnlyKeys(HttpHeaders.AUTHORIZATION); + } + + @Test + public void mixedCaseAuthorizationHeaderCanBeIncluded() { + HttpTrace trace = new HttpExchangeTracer( + EnumSet.of(Include.REQUEST_HEADERS, Include.AUTHORIZATION_HEADER)) + .receivedRequest(createRequest(Collections.singletonMap( + mixedCase(HttpHeaders.AUTHORIZATION), + Arrays.asList("secret")))); + Request request = trace.getRequest(); + assertThat(request.getHeaders()) + .containsOnlyKeys(mixedCase(HttpHeaders.AUTHORIZATION)); + } + + @Test + public void cookieHeaderIsNotIncludedByDefault() { + HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REQUEST_HEADERS)) + .receivedRequest(createRequest(Collections + .singletonMap(HttpHeaders.COOKIE, Arrays.asList("test=test")))); + Request request = trace.getRequest(); + assertThat(request.getHeaders()).isEmpty(); + } + + @Test + public void mixedCaseCookieHeaderIsNotIncludedByDefault() { + HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REQUEST_HEADERS)) + .receivedRequest(createRequest(Collections.singletonMap( + mixedCase(HttpHeaders.COOKIE), Arrays.asList("valuet")))); + Request request = trace.getRequest(); + assertThat(request.getHeaders()).isEmpty(); + } + + @Test + public void cookieHeaderCanBeIncluded() { + HttpTrace trace = new HttpExchangeTracer( + EnumSet.of(Include.REQUEST_HEADERS, Include.COOKIE_HEADERS)) + .receivedRequest(createRequest(Collections.singletonMap( + HttpHeaders.COOKIE, Arrays.asList("value")))); + Request request = trace.getRequest(); + assertThat(request.getHeaders()).containsOnlyKeys(HttpHeaders.COOKIE); + } + + @Test + public void mixedCaseCookieHeaderCanBeIncluded() { + HttpTrace trace = new HttpExchangeTracer( + EnumSet.of(Include.REQUEST_HEADERS, Include.COOKIE_HEADERS)) + .receivedRequest(createRequest(Collections.singletonMap( + mixedCase(HttpHeaders.COOKIE), Arrays.asList("value")))); + Request request = trace.getRequest(); + assertThat(request.getHeaders()).containsOnlyKeys(mixedCase(HttpHeaders.COOKIE)); + } + + @Test + public void statusIsIncluded() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace, + createResponse(), null, null); + assertThat(trace.getResponse().getStatus()).isEqualTo(204); + } + + @Test + public void responseHeadersAreNotIncludedByDefault() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace, + createResponse(), null, null); + assertThat(trace.getResponse().getHeaders()).isEmpty(); + } + + @Test + public void responseHeadersCanBeIncluded() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer(EnumSet.of(Include.RESPONSE_HEADERS)) + .sendingResponse(trace, createResponse(), null, null); + assertThat(trace.getResponse().getHeaders()) + .containsOnlyKeys(HttpHeaders.CONTENT_TYPE); + } + + @Test + public void setCookieHeaderIsNotIncludedByDefault() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer(EnumSet.of(Include.RESPONSE_HEADERS)).sendingResponse( + trace, createResponse(Collections.singletonMap(HttpHeaders.SET_COOKIE, + Arrays.asList("test=test"))), + null, null); + assertThat(trace.getResponse().getHeaders()).isEmpty(); + } + + @Test + public void mixedCaseSetCookieHeaderIsNotIncludedByDefault() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer(EnumSet.of(Include.RESPONSE_HEADERS)) + .sendingResponse(trace, + createResponse(Collections.singletonMap( + mixedCase(HttpHeaders.SET_COOKIE), + Arrays.asList("test=test"))), + null, null); + assertThat(trace.getResponse().getHeaders()).isEmpty(); + } + + @Test + public void setCookieHeaderCanBeIncluded() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer( + EnumSet.of(Include.RESPONSE_HEADERS, Include.COOKIE_HEADERS)) + .sendingResponse(trace, + createResponse( + Collections.singletonMap(HttpHeaders.SET_COOKIE, + Arrays.asList("test=test"))), + null, null); + assertThat(trace.getResponse().getHeaders()) + .containsOnlyKeys(HttpHeaders.SET_COOKIE); + } + + @Test + public void mixedCaseSetCookieHeaderCanBeIncluded() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer( + EnumSet.of(Include.RESPONSE_HEADERS, Include.COOKIE_HEADERS)) + .sendingResponse(trace, + createResponse(Collections.singletonMap( + mixedCase(HttpHeaders.SET_COOKIE), + Arrays.asList("test=test"))), + null, null); + assertThat(trace.getResponse().getHeaders()) + .containsOnlyKeys(mixedCase(HttpHeaders.SET_COOKIE)); + } + + @Test + public void principalIsNotIncludedByDefault() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace, + createResponse(), () -> createPrincipal(), null); + assertThat(trace.getPrincipal()).isNull(); + } + + @Test + public void principalCanBeIncluded() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer(EnumSet.of(Include.PRINCIPAL)).sendingResponse(trace, + createResponse(), () -> createPrincipal(), null); + assertThat(trace.getPrincipal()).isNotNull(); + assertThat(trace.getPrincipal().getName()).isEqualTo("alice"); + } + + @Test + public void sessionIdIsNotIncludedByDefault() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace, + createResponse(), null, () -> "sessionId"); + assertThat(trace.getSession()).isNull(); + } + + @Test + public void sessionIdCanBeIncluded() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer(EnumSet.of(Include.SESSION_ID)).sendingResponse(trace, + createResponse(), null, () -> "sessionId"); + assertThat(trace.getSession()).isNotNull(); + assertThat(trace.getSession().getId()).isEqualTo("sessionId"); + } + + @Test + public void timeTakenIsNotIncludedByDefault() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace, + createResponse(), null, null); + assertThat(trace.getTimeTaken()).isNull(); + } + + @Test + public void timeTakedCanBeIncluded() { + HttpTrace trace = new HttpTrace(createRequest()); + new HttpExchangeTracer(EnumSet.of(Include.TIME_TAKEN)).sendingResponse(trace, + createResponse(), null, null); + assertThat(trace.getTimeTaken()).isNotNull(); + } + + private TraceableRequest createRequest() { + return createRequest(Collections.singletonMap(HttpHeaders.ACCEPT, + Arrays.asList("application/json"))); + } + + private TraceableRequest createRequest(Map> headers) { + TraceableRequest request = mock(TraceableRequest.class); + given(request.getMethod()).willReturn("GET"); + given(request.getRemoteAddress()).willReturn("127.0.0.1"); + given(request.getHeaders()).willReturn(new HashMap<>(headers)); + given(request.getUri()).willReturn(URI.create("https://api.example.com")); + return request; + } + + private TraceableResponse createResponse() { + return createResponse(Collections.singletonMap(HttpHeaders.CONTENT_TYPE, + Arrays.asList("application/json"))); + } + + private TraceableResponse createResponse(Map> headers) { + TraceableResponse response = mock(TraceableResponse.class); + given(response.getStatus()).willReturn(204); + given(response.getHeaders()).willReturn(new HashMap<>(headers)); + return response; + } + + private Principal createPrincipal() { + Principal principal = mock(Principal.class); + given(principal.getName()).willReturn("alice"); + return principal; + } + + private String mixedCase(String input) { + StringBuilder output = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + output.append(i % 2 == 0 ? Character.toLowerCase(input.charAt(i)) + : Character.toUpperCase(input.charAt(i))); + } + return output.toString(); + } +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/InMemoryTraceRepositoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/InMemoryTraceRepositoryTests.java new file mode 100644 index 0000000000..9256bb628e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/InMemoryTraceRepositoryTests.java @@ -0,0 +1,68 @@ +/* + * 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.actuate.web.trace; + +import java.util.List; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link InMemoryHttpTraceRepository}. + * + * @author Dave Syer + * @author Andy Wilkinson + */ +public class InMemoryTraceRepositoryTests { + + private final InMemoryHttpTraceRepository repository = new InMemoryHttpTraceRepository(); + + @Test + public void capacityLimited() { + this.repository.setCapacity(2); + this.repository.add(new HttpTrace(createRequest("GET"))); + this.repository.add(new HttpTrace(createRequest("POST"))); + this.repository.add(new HttpTrace(createRequest("DELETE"))); + List traces = this.repository.findAll(); + assertThat(traces).hasSize(2); + assertThat(traces.get(0).getRequest().getMethod()).isEqualTo("DELETE"); + assertThat(traces.get(1).getRequest().getMethod()).isEqualTo("POST"); + } + + @Test + public void reverseFalse() { + this.repository.setReverse(false); + this.repository.setCapacity(2); + this.repository.add(new HttpTrace(createRequest("GET"))); + this.repository.add(new HttpTrace(createRequest("POST"))); + this.repository.add(new HttpTrace(createRequest("DELETE"))); + List traces = this.repository.findAll(); + assertThat(traces).hasSize(2); + assertThat(traces.get(0).getRequest().getMethod()).isEqualTo("POST"); + assertThat(traces.get(1).getRequest().getMethod()).isEqualTo("DELETE"); + } + + private TraceableRequest createRequest(String method) { + TraceableRequest request = mock(TraceableRequest.class); + given(request.getMethod()).willReturn(method); + return request; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/TraceEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/TraceEndpointTests.java new file mode 100644 index 0000000000..87d86fc72a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/TraceEndpointTests.java @@ -0,0 +1,51 @@ +/* + * 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.actuate.web.trace; + +import java.util.List; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HttpTraceEndpoint}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +public class TraceEndpointTests { + + @Test + public void trace() { + HttpTraceRepository repository = new InMemoryHttpTraceRepository(); + repository.add(new HttpTrace(createRequest("GET"))); + List traces = new HttpTraceEndpoint(repository).traces().getTraces(); + assertThat(traces).hasSize(1); + HttpTrace trace = traces.get(0); + assertThat(trace.getRequest().getMethod()).isEqualTo("GET"); + } + + private TraceableRequest createRequest(String method) { + TraceableRequest request = mock(TraceableRequest.class); + given(request.getMethod()).willReturn(method); + return request; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/HttpTraceWebFilterIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/HttpTraceWebFilterIntegrationTests.java new file mode 100644 index 0000000000..109d036638 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/HttpTraceWebFilterIntegrationTests.java @@ -0,0 +1,131 @@ +/* + * 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.actuate.web.trace.reactive; + +import java.util.EnumSet; +import java.util.Set; + +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.web.trace.HttpExchangeTracer; +import org.springframework.boot.actuate.web.trace.HttpTraceRepository; +import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository; +import org.springframework.boot.actuate.web.trace.Include; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link HttpTraceWebFilter}. + * + * @author Andy Wilkinson + */ +public class HttpTraceWebFilterIntegrationTests { + + @Test + public void traceForNotFoundResponseHas404Status() { + ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner() + .withUserConfiguration(Config.class); + runner.run((context) -> { + WebTestClient.bindToApplicationContext(context).build().get().uri("/") + .exchange().expectStatus().isNotFound(); + HttpTraceRepository repository = context.getBean(HttpTraceRepository.class); + assertThat(repository.findAll()).hasSize(1); + assertThat(repository.findAll().get(0).getResponse().getStatus()) + .isEqualTo(404); + }); + } + + @Test + public void traceForMonoErrorWithRuntimeExceptionHas500Status() { + ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner() + .withUserConfiguration(Config.class); + runner.run((context) -> { + WebTestClient.bindToApplicationContext(context).build().get() + .uri("/mono-error").exchange().expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + HttpTraceRepository repository = context.getBean(HttpTraceRepository.class); + assertThat(repository.findAll()).hasSize(1); + assertThat(repository.findAll().get(0).getResponse().getStatus()) + .isEqualTo(500); + }); + } + + @Test + public void traceForThrownRuntimeExceptionHas500Status() { + ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner() + .withUserConfiguration(Config.class); + runner.run((context) -> { + WebTestClient.bindToApplicationContext(context).build().get().uri("/thrown") + .exchange().expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + HttpTraceRepository repository = context.getBean(HttpTraceRepository.class); + assertThat(repository.findAll()).hasSize(1); + assertThat(repository.findAll().get(0).getResponse().getStatus()) + .isEqualTo(500); + }); + } + + @Configuration + @EnableWebFlux + static class Config { + + @Bean + public HttpTraceWebFilter httpTraceWebFilter(HttpTraceRepository repository) { + Set includes = EnumSet.allOf(Include.class); + return new HttpTraceWebFilter(repository, new HttpExchangeTracer(includes), + includes); + } + + @Bean + public HttpTraceRepository httpTraceRepository() { + return new InMemoryHttpTraceRepository(); + } + + @Bean + public HttpHandler httpHandler(ApplicationContext applicationContext) { + return WebHttpHandlerBuilder.applicationContext(applicationContext).build(); + } + + @Bean + public RouterFunction router() { + return RouterFunctions + .route(RequestPredicates.GET("/mono-error"), + (request) -> Mono.error(new RuntimeException())) + .andRoute(RequestPredicates.GET("/thrown"), + (HandlerFunction) (request) -> { + throw new RuntimeException(); + }); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/HttpTraceWebFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/HttpTraceWebFilterTests.java new file mode 100644 index 0000000000..4c93bb1059 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/HttpTraceWebFilterTests.java @@ -0,0 +1,167 @@ +/* + * 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.actuate.web.trace.reactive; + +import java.io.IOException; +import java.security.Principal; +import java.util.EnumSet; + +import javax.servlet.ServletException; + +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.web.trace.HttpExchangeTracer; +import org.springframework.boot.actuate.web.trace.HttpTrace.Session; +import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository; +import org.springframework.boot.actuate.web.trace.Include; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebExchangeDecorator; +import org.springframework.web.server.WebFilterChain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HttpTraceWebFilter}. + * + * @author Andy Wilkinson + */ +public class HttpTraceWebFilterTests { + + private final InMemoryHttpTraceRepository repository = new InMemoryHttpTraceRepository(); + + private final HttpExchangeTracer tracer = new HttpExchangeTracer( + EnumSet.allOf(Include.class)); + + private final HttpTraceWebFilter filter = new HttpTraceWebFilter(this.repository, + this.tracer, EnumSet.allOf(Include.class)); + + @Test + public void filterTracesExchange() throws ServletException, IOException { + this.filter.filter( + MockServerWebExchange + .from(MockServerHttpRequest.get("https://api.example.com")), + new WebFilterChain() { + + @Override + public Mono filter(ServerWebExchange exchange) { + return Mono.empty(); + } + + }).block(); + assertThat(this.repository.findAll()).hasSize(1); + } + + @Test + public void filterCapturesSessionIdWhenSessionIsUsed() + throws ServletException, IOException { + this.filter.filter( + MockServerWebExchange + .from(MockServerHttpRequest.get("https://api.example.com")), + new WebFilterChain() { + + @Override + public Mono filter(ServerWebExchange exchange) { + exchange.getSession().block().getAttributes().put("a", "alpha"); + return Mono.empty(); + } + + }).block(); + assertThat(this.repository.findAll()).hasSize(1); + Session session = this.repository.findAll().get(0).getSession(); + assertThat(session).isNotNull(); + assertThat(session.getId()).isNotNull(); + } + + @Test + public void filterDoesNotCaptureIdOfUnusedSession() + throws ServletException, IOException { + this.filter.filter( + MockServerWebExchange + .from(MockServerHttpRequest.get("https://api.example.com")), + new WebFilterChain() { + + @Override + public Mono filter(ServerWebExchange exchange) { + exchange.getSession().block(); + return Mono.empty(); + } + + }).block(); + assertThat(this.repository.findAll()).hasSize(1); + Session session = this.repository.findAll().get(0).getSession(); + assertThat(session).isNull(); + } + + @Test + public void filterCapturesPrincipal() throws ServletException, IOException { + Principal principal = mock(Principal.class); + given(principal.getName()).willReturn("alice"); + this.filter.filter(new ServerWebExchangeDecorator(MockServerWebExchange + .from(MockServerHttpRequest.get("https://api.example.com"))) { + + @Override + public Mono getPrincipal() { + return Mono.just(principal); + } + + }, new WebFilterChain() { + + @Override + public Mono filter(ServerWebExchange exchange) { + exchange.getSession().block().getAttributes().put("a", "alpha"); + return Mono.empty(); + } + + }).block(); + assertThat(this.repository.findAll()).hasSize(1); + org.springframework.boot.actuate.web.trace.HttpTrace.Principal tracedPrincipal = this.repository + .findAll().get(0).getPrincipal(); + assertThat(tracedPrincipal).isNotNull(); + assertThat(tracedPrincipal.getName()).isEqualTo("alice"); + } + + @Test + public void statusIsAssumedToBe500WhenChainFails() + throws ServletException, IOException { + try { + this.filter.filter( + MockServerWebExchange + .from(MockServerHttpRequest.get("https://api.example.com")), + new WebFilterChain() { + + @Override + public Mono filter(ServerWebExchange exchange) { + return Mono.error(new RuntimeException()); + } + + }).block(); + fail(); + } + catch (Exception ex) { + assertThat(this.repository.findAll()).hasSize(1); + assertThat(this.repository.findAll().get(0).getResponse().getStatus()) + .isEqualTo(500); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/servlet/HttpTraceFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/servlet/HttpTraceFilterTests.java new file mode 100644 index 0000000000..d82a35123e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/servlet/HttpTraceFilterTests.java @@ -0,0 +1,129 @@ +/* + * 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.actuate.web.trace.servlet; + +import java.io.IOException; +import java.security.Principal; +import java.util.EnumSet; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; + +import org.springframework.boot.actuate.web.trace.HttpExchangeTracer; +import org.springframework.boot.actuate.web.trace.HttpTrace.Session; +import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository; +import org.springframework.boot.actuate.web.trace.Include; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HttpTraceFilter}. + * + * @author Dave Syer + * @author Wallace Wadge + * @author Phillip Webb + * @author Andy Wilkinson + * @author Venil Noronha + * @author Stephane Nicoll + * @author Madhura Bhave + */ +public class HttpTraceFilterTests { + + private final InMemoryHttpTraceRepository repository = new InMemoryHttpTraceRepository(); + + private final HttpExchangeTracer tracer = new HttpExchangeTracer( + EnumSet.allOf(Include.class)); + + private final HttpTraceFilter filter = new HttpTraceFilter(this.repository, + this.tracer); + + @Test + public void filterTracesExchange() throws ServletException, IOException { + this.filter.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(), + new MockFilterChain()); + assertThat(this.repository.findAll()).hasSize(1); + } + + @Test + public void filterCapturesSessionId() throws ServletException, IOException { + this.filter.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(), + new MockFilterChain(new HttpServlet() { + + @Override + protected void service(HttpServletRequest req, + HttpServletResponse resp) + throws ServletException, IOException { + req.getSession(true); + } + + })); + assertThat(this.repository.findAll()).hasSize(1); + Session session = this.repository.findAll().get(0).getSession(); + assertThat(session).isNotNull(); + assertThat(session.getId()).isNotNull(); + } + + @Test + public void filterCapturesPrincipal() throws ServletException, IOException { + MockHttpServletRequest request = new MockHttpServletRequest(); + Principal principal = mock(Principal.class); + given(principal.getName()).willReturn("alice"); + request.setUserPrincipal(principal); + this.filter.doFilter(request, new MockHttpServletResponse(), + new MockFilterChain()); + assertThat(this.repository.findAll()).hasSize(1); + org.springframework.boot.actuate.web.trace.HttpTrace.Principal tracedPrincipal = this.repository + .findAll().get(0).getPrincipal(); + assertThat(tracedPrincipal).isNotNull(); + assertThat(tracedPrincipal.getName()).isEqualTo("alice"); + } + + @Test + public void statusIsAssumedToBe500WhenChainFails() + throws ServletException, IOException { + try { + this.filter.doFilter(new MockHttpServletRequest(), + new MockHttpServletResponse(), new MockFilterChain(new HttpServlet() { + + @Override + protected void service(HttpServletRequest req, + HttpServletResponse resp) + throws ServletException, IOException { + throw new IOException(); + } + + })); + fail("Filter swallowed IOException"); + } + catch (IOException ex) { + assertThat(this.repository.findAll()).hasSize(1); + assertThat(this.repository.findAll().get(0).getResponse().getStatus()) + .isEqualTo(500); + } + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index 67f8a5da7e..38fc8f3192 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -115,7 +115,7 @@ store. Not available when using Spring Session's support for reactive web applic |Performs a thread dump. |`trace` -|Displays trace information (by default, the last 100 HTTP requests). +|Displays HTTP trace information (by default, the last 100 HTTP requests). |=== If your application is a web application (Spring MVC, Spring WebFlux, or Jersey), you can @@ -1161,71 +1161,14 @@ implementing `ApplicationEventPublisherAware`). [[production-ready-tracing]] == Tracing Tracing is automatically enabled for all HTTP requests. You can view the `trace` endpoint -and obtain basic information about the last 100 requests. The following listing shows -sample output: - -[source,json,indent=0] ----- - [{ - "timestamp": 1394343677415, - "info": { - "method": "GET", - "path": "/trace", - "headers": { - "request": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Connection": "keep-alive", - "Accept-Encoding": "gzip, deflate", - "User-Agent": "Mozilla/5.0 Gecko/Firefox", - "Accept-Language": "en-US,en;q=0.5", - "Cookie": "_ga=GA1.1.827067509.1390890128; ..." - "Authorization": "Basic ...", - "Host": "localhost:8080" - }, - "response": { - "Strict-Transport-Security": "max-age=31536000 ; includeSubDomains", - "X-Application-Context": "application:8080", - "Content-Type": "application/json;charset=UTF-8", - "status": "200" - } - } - } - },{ - "timestamp": 1394343684465, - ... - }] ----- - -By default, the trace includes the following information: - -[cols="1,2"] -|=== -|Name |Description - -|Request Headers -|Headers from the request. - -|Response Headers -|Headers from the response. - -|Cookies -|`Cookie` from request headers and `Set-Cookie` from response headers. - -|Errors -|The error attributes (if any). - -|Time Taken -|The time taken to service the request in milliseconds. -|=== +and obtain basic information about the last 100 requests. [[production-ready-custom-tracing]] === Custom tracing -If you need to trace additional events, you can inject a -{sc-spring-boot-actuator}/trace/TraceRepository.{sc-ext}[`TraceRepository`] into your -Spring beans. The `add` method accepts a single `Map` structure that is converted to JSON -and logged. +To customize the items that are included in each trace, use the +`management.trace.include` configuration property. By default, an `InMemoryTraceRepository` that stores the last 100 events is used. If you need to expand the capacity, you can define your own instance of the diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 1fb1b90f45..44dfc1bce7 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -2660,6 +2660,9 @@ does so, the orders shown in the following table will be used: |`WebFilterChainProxy` (Spring Security) |`-100` +|`HttpTraceWebFilter` +|`Ordered.LOWEST_PRECEDENCE - 10` + |=== @@ -2794,7 +2797,7 @@ precedence): |`ErrorPageFilter` |`Ordered.HIGHEST_PRECEDENCE + 1` -|`WebRequestTraceFilter` +|`HttpTraceFilter` |`Ordered.LOWEST_PRECEDENCE - 10` |=== 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 7da56b2085..50cdf608ed 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 @@ -18,6 +18,4 @@ spring.jmx.enabled=true spring.jackson.serialization.write_dates_as_timestamps=false -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 +management.trace.include=REQUEST_HEADERS,RESPONSE_HEADERS,PRINCIPAL,REMOTE_ADDRESS,SESSION_ID diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/SampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/SampleActuatorApplicationTests.java index b49be6d0ee..5102afc891 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/SampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/SampleActuatorApplicationTests.java @@ -166,41 +166,6 @@ public class SampleActuatorApplicationTests { assertThat(body).contains("This application has no explicit mapping for /error"); } - @Test - @SuppressWarnings("unchecked") - public void testTrace() { - this.restTemplate.getForEntity("/health", String.class); - @SuppressWarnings("rawtypes") - ResponseEntity entity = this.restTemplate - .withBasicAuth("user", getPassword()) - .getForEntity("/actuator/trace", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - Map body = entity.getBody(); - Map trace = ((List>) body.get("traces")) - .get(0); - Map map = (Map) ((Map) ((Map) trace - .get("info")).get("headers")).get("response"); - assertThat(map.get("status")).isEqualTo("200"); - } - - @Test - @SuppressWarnings("unchecked") - public void traceWithParameterMap() { - this.restTemplate.withBasicAuth("user", getPassword()) - .getForEntity("/actuator/health?param1=value1", String.class); - @SuppressWarnings("rawtypes") - ResponseEntity entity = this.restTemplate - .withBasicAuth("user", getPassword()) - .getForEntity("/actuator/trace", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - Map body = entity.getBody(); - Map trace = ((List>) body.get("traces")) - .get(0); - Map map = (Map) ((Map) trace - .get("info")).get("parameters"); - assertThat(map.get("param1")).isNotNull(); - } - @Test public void testErrorPageDirectAccess() { @SuppressWarnings("rawtypes")