diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/anchor-rewrite.properties b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/anchor-rewrite.properties index 74fbed4f09..d68c413f9e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/anchor-rewrite.properties +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/anchor-rewrite.properties @@ -132,3 +132,7 @@ threaddump=threaddump threaddump-retrieving-json=threaddump.retrieving-json threaddump-retrieving-json-response-structure=threaddump.retrieving-json.response-structure threaddump-retrieving-text=threaddump.retrieving-text +http-trace=httpexchanges +http-trace.retrieving=httpexchanges.retrieving +http-trace.retrieving.response-structure=httpexchanges.retrieving.response-structure + diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/httpexchanges.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/httpexchanges.adoc new file mode 100644 index 0000000000..ec07ddf327 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/httpexchanges.adoc @@ -0,0 +1,25 @@ +[[httpexchanges]] += HTTP Exchanges (`httpexchanges`) +The `httpexchanges` endpoint provides information about HTTP request-response exchanges. + + + +[[httpexchanges.retrieving]] +== Retrieving the HTTP Exchanges +To retrieve the HTTP exchanges, make a `GET` request to `/actuator/httpexchanges`, as shown in the following curl-based example: + +include::{snippets}/httpexchanges/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/httpexchanges/http-response.adoc[] + + + +[[httpexchanges.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}/httpexchanges/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/httptrace.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/httptrace.adoc deleted file mode 100644 index fc566a0121..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/httptrace.adoc +++ /dev/null @@ -1,25 +0,0 @@ -[[http-trace]] -= HTTP Trace (`httptrace`) -The `httptrace` endpoint provides information about HTTP request-response exchanges. - - - -[[http-trace.retrieving]] -== Retrieving the Traces -To retrieve the traces, make a `GET` request to `/actuator/httptrace`, as shown in the following curl-based example: - -include::{snippets}/httptrace/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}/httptrace/http-response.adoc[] - - - -[[http-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}/httptrace/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/index.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/index.adoc index 0168b94a4a..daf6fa9fed 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/index.adoc +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/index.adoc @@ -72,7 +72,7 @@ include::endpoints/health.adoc[leveloffset=+1] include::endpoints/heapdump.adoc[leveloffset=+1] -include::endpoints/httptrace.adoc[leveloffset=+1] +include::endpoints/httpexchanges.adoc[leveloffset=+1] include::endpoints/info.adoc[leveloffset=+1] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesAutoConfiguration.java similarity index 56% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceAutoConfiguration.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesAutoConfiguration.java index 098926baf6..5b075d2782 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesAutoConfiguration.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.boot.actuate.autoconfigure.trace.http; +package org.springframework.boot.actuate.autoconfigure.web.exchanges; -import org.springframework.boot.actuate.trace.http.HttpExchangeTracer; -import org.springframework.boot.actuate.trace.http.HttpTraceRepository; -import org.springframework.boot.actuate.web.trace.reactive.HttpTraceWebFilter; -import org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter; +import org.springframework.boot.actuate.web.exchanges.HttpExchange; +import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository; +import org.springframework.boot.actuate.web.exchanges.reactive.HttpExchangesWebFilter; +import org.springframework.boot.actuate.web.exchanges.servlet.HttpExchangesFilter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -32,45 +32,41 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** - * {@link EnableAutoConfiguration Auto-configuration} for HTTP tracing. + * {@link EnableAutoConfiguration Auto-configuration} to record {@link HttpExchange HTTP + * exchanges}. * * @author Dave Syer - * @since 2.0.0 + * @since 3.0.0 */ @AutoConfiguration @ConditionalOnWebApplication -@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true) -@ConditionalOnBean(HttpTraceRepository.class) -@EnableConfigurationProperties(HttpTraceProperties.class) -public class HttpTraceAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public HttpExchangeTracer httpExchangeTracer(HttpTraceProperties traceProperties) { - return new HttpExchangeTracer(traceProperties.getInclude()); - } +@ConditionalOnProperty(prefix = "management.httpexchanges", name = "record", matchIfMissing = true) +@ConditionalOnBean(HttpExchangeRepository.class) +@EnableConfigurationProperties(HttpExchangesProperties.class) +public class HttpExchangesAutoConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) - static class ServletTraceFilterConfiguration { + static class ServletHttpExchangesConfiguration { @Bean @ConditionalOnMissingBean - HttpTraceFilter httpTraceFilter(HttpTraceRepository repository, HttpExchangeTracer tracer) { - return new HttpTraceFilter(repository, tracer); + HttpExchangesFilter httpExchangesFilter(HttpExchangeRepository repository, + HttpExchangesProperties traceProperties) { + return new HttpExchangesFilter(repository, traceProperties.getInclude()); } } @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.REACTIVE) - static class ReactiveTraceFilterConfiguration { + static class ReactiveHttpExchangesConfiguration { @Bean @ConditionalOnMissingBean - HttpTraceWebFilter httpTraceWebFilter(HttpTraceRepository repository, HttpExchangeTracer tracer, - HttpTraceProperties traceProperties) { - return new HttpTraceWebFilter(repository, tracer, traceProperties.getInclude()); + HttpExchangesWebFilter httpExchangesWebFilter(HttpExchangeRepository repository, + HttpExchangesProperties traceProperties) { + return new HttpExchangesWebFilter(repository, traceProperties.getInclude()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesEndpointAutoConfiguration.java similarity index 61% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceEndpointAutoConfiguration.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesEndpointAutoConfiguration.java index e2bda1fab8..137d1b3234 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesEndpointAutoConfiguration.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.boot.actuate.autoconfigure.trace.http; +package org.springframework.boot.actuate.autoconfigure.web.exchanges; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; -import org.springframework.boot.actuate.trace.http.HttpTraceEndpoint; -import org.springframework.boot.actuate.trace.http.HttpTraceRepository; +import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository; +import org.springframework.boot.actuate.web.exchanges.HttpExchangesEndpoint; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -26,20 +26,21 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.context.annotation.Bean; /** - * {@link EnableAutoConfiguration Auto-configuration} for the {@link HttpTraceEndpoint}. + * {@link EnableAutoConfiguration Auto-configuration} for the + * {@link HttpExchangesEndpoint}. * * @author Phillip Webb - * @since 2.0.0 + * @since 3.0.0 */ -@AutoConfiguration(after = HttpTraceAutoConfiguration.class) -@ConditionalOnAvailableEndpoint(endpoint = HttpTraceEndpoint.class) -public class HttpTraceEndpointAutoConfiguration { +@AutoConfiguration(after = HttpExchangesAutoConfiguration.class) +@ConditionalOnAvailableEndpoint(endpoint = HttpExchangesEndpoint.class) +public class HttpExchangesEndpointAutoConfiguration { @Bean - @ConditionalOnBean(HttpTraceRepository.class) + @ConditionalOnBean(HttpExchangeRepository.class) @ConditionalOnMissingBean - public HttpTraceEndpoint httpTraceEndpoint(HttpTraceRepository traceRepository) { - return new HttpTraceEndpoint(traceRepository); + public HttpExchangesEndpoint httpExchangesEndpoint(HttpExchangeRepository exchangeRepository) { + return new HttpExchangesEndpoint(exchangeRepository); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesProperties.java similarity index 81% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceProperties.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesProperties.java index 4a3f139127..9799acb065 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2022 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.http; +package org.springframework.boot.actuate.autoconfigure.web.exchanges; import java.util.HashSet; import java.util.Set; -import org.springframework.boot.actuate.trace.http.Include; +import org.springframework.boot.actuate.web.exchanges.Include; import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -32,8 +32,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * @author Stephane Nicoll * @since 2.0.0 */ -@ConfigurationProperties(prefix = "management.trace.http") -public class HttpTraceProperties { +@ConfigurationProperties(prefix = "management.httpexchanges") +public class HttpExchangesProperties { /** * Items to be included in the trace. Defaults to request headers (excluding diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/package-info.java similarity index 76% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/package-info.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/package-info.java index fa32e46442..c696d77a6c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/package-info.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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,6 +15,6 @@ */ /** - * Auto-configuration for actuator HTTP tracing concerns. + * Auto-configuration for actuator HTTP exchanges. */ -package org.springframework.boot.actuate.autoconfigure.trace.http; +package org.springframework.boot.actuate.autoconfigure.web.exchanges; 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 886cb73afe..2b21e01e49 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 @@ -239,6 +239,20 @@ "description": "Whether to enable Redis health check.", "defaultValue": true }, + { + "name": "management.httpexchanges.include", + "defaultValue": [ + "request-headers", + "response-headers", + "errors" + ] + }, + { + "name": "management.httpexchanges.record", + "type": "java.lang.Boolean", + "description": "Whether to record HTTP request-response exchanges.", + "defaultValue": true + }, { "name": "management.influx.metrics.export.consistency", "defaultValue": "one" @@ -2126,22 +2140,22 @@ }, { "name": "management.trace.http.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable HTTP request-response tracing.", - "defaultValue": true + "deprecation": { + "replacement": "management.httpexchanges.record", + "level": "error" + } }, { "name": "management.trace.http.include", - "defaultValue": [ - "request-headers", - "response-headers", - "errors" - ] + "deprecation": { + "replacement": "management.httpexchanges.include", + "level": "error" + } }, { "name": "management.trace.include", "deprecation": { - "replacement": "management.trace.http.include", + "replacement": "management.httpexchanges.include", "level": "error" } }, diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 74ec09657f..2da89f0835 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -96,14 +96,14 @@ org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSec org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.startup.StartupEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration -org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceAutoConfiguration -org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.exemplars.ExemplarsAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.wavefront.WavefrontTracingAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration +org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesAutoConfiguration +org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementContextAutoConfiguration org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HttpExchangesEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HttpExchangesEndpointDocumentationTests.java new file mode 100644 index 0000000000..c877db48be --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HttpExchangesEndpointDocumentationTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation; + +import java.net.URI; +import java.security.Principal; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.web.exchanges.HttpExchange; +import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository; +import org.springframework.boot.actuate.web.exchanges.HttpExchangesEndpoint; +import org.springframework.boot.actuate.web.exchanges.Include; +import org.springframework.boot.actuate.web.exchanges.SourceHttpRequest; +import org.springframework.boot.actuate.web.exchanges.SourceHttpResponse; +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 HttpExchangesEndpoint}. + * + * @author Andy Wilkinson + */ +class HttpExchangesEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { + + @MockBean + private HttpExchangeRepository repository; + + @Test + void httpExchanges() throws Exception { + SourceHttpRequest request = mock(SourceHttpRequest.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"))); + SourceHttpResponse response = mock(SourceHttpResponse.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"); + Instant instant = Instant.parse("2022-12-22T13:43:41.00Z"); + Clock start = Clock.fixed(instant, ZoneId.systemDefault()); + Clock end = Clock.offset(start, Duration.ofMillis(23)); + HttpExchange exchange = HttpExchange.start(start, request).finish(end, response, () -> principal, + () -> UUID.randomUUID().toString(), EnumSet.allOf(Include.class)); + given(this.repository.findAll()).willReturn(Arrays.asList(exchange)); + this.mockMvc.perform(get("/actuator/httpexchanges")).andExpect(status().isOk()).andDo(document("httpexchanges", + responseFields(fieldWithPath("exchanges").description("An array of HTTP request-response exchanges."), + fieldWithPath("exchanges.[].timestamp").description("Timestamp of when the exchange occurred."), + fieldWithPath("exchanges.[].principal").description("Principal of the exchange, if any.") + .optional(), + fieldWithPath("exchanges.[].principal.name").description("Name of the principal.").optional(), + fieldWithPath("exchanges.[].request.method").description("HTTP method of the request."), + fieldWithPath("exchanges.[].request.remoteAddress") + .description("Remote address from which the request was received, if known.").optional() + .type(JsonFieldType.STRING), + fieldWithPath("exchanges.[].request.uri").description("URI of the request."), + fieldWithPath("exchanges.[].request.headers") + .description("Headers of the request, keyed by header name."), + fieldWithPath("exchanges.[].request.headers.*.[]").description("Values of the header"), + fieldWithPath("exchanges.[].response.status").description("Status of the response"), + fieldWithPath("exchanges.[].response.headers") + .description("Headers of the response, keyed by header name."), + fieldWithPath("exchanges.[].response.headers.*.[]").description("Values of the header"), + fieldWithPath("exchanges.[].session") + .description("Session associated with the exchange, if any.").optional(), + fieldWithPath("exchanges.[].session.id").description("ID of the session."), + fieldWithPath("exchanges.[].timeTaken").description("Time taken to handle the exchange.")))); + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseDocumentationConfiguration.class) + static class TestConfiguration { + + @Bean + HttpExchangesEndpoint httpExchangesEndpoint(HttpExchangeRepository repository) { + return new HttpExchangesEndpoint(repository); + } + + } + +} 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 deleted file mode 100644 index 0117a9a272..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HttpTraceEndpointDocumentationTests.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.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.jupiter.api.Test; - -import org.springframework.boot.actuate.trace.http.HttpExchangeTracer; -import org.springframework.boot.actuate.trace.http.HttpTrace; -import org.springframework.boot.actuate.trace.http.HttpTraceEndpoint; -import org.springframework.boot.actuate.trace.http.HttpTraceRepository; -import org.springframework.boot.actuate.trace.http.Include; -import org.springframework.boot.actuate.trace.http.TraceableRequest; -import org.springframework.boot.actuate.trace.http.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 - */ -class HttpTraceEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { - - @MockBean - private HttpTraceRepository repository; - - @Test - 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/httptrace")).andExpect(status().isOk()) - .andDo(document("httptrace", 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(proxyBeanMethods = false) - @Import(BaseDocumentationConfiguration.class) - static class TestConfiguration { - - @Bean - 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 f87fd7aadc..5ae4537f3d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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,7 +28,7 @@ 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.http.HttpTraceEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration; import org.springframework.util.ClassUtils; @@ -50,7 +50,7 @@ final class EndpointAutoConfigurationClasses { all.add(HealthEndpointAutoConfiguration.class); all.add(InfoEndpointAutoConfiguration.class); all.add(ThreadDumpEndpointAutoConfiguration.class); - all.add(HttpTraceEndpointAutoConfiguration.class); + all.add(HttpExchangesEndpointAutoConfiguration.class); all.add(MappingsEndpointAutoConfiguration.class); ALL = ClassUtils.toClassArray(all); } 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 7a4dce3d53..04529c295f 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 @@ -31,8 +31,8 @@ import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceAutoConfiguration; -import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository; +import org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesAutoConfiguration; +import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -53,8 +53,8 @@ class JmxEndpointIntegrationTests { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(JmxAutoConfiguration.class, EndpointAutoConfiguration.class, JmxEndpointAutoConfiguration.class, HealthContributorAutoConfiguration.class, - HttpTraceAutoConfiguration.class)) - .withUserConfiguration(HttpTraceRepositoryConfiguration.class, AuditEventRepositoryConfiguration.class) + HttpExchangesAutoConfiguration.class)) + .withUserConfiguration(HttpExchangeRepositoryConfiguration.class, AuditEventRepositoryConfiguration.class) .withPropertyValues("spring.jmx.enabled=true") .withConfiguration(AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL)); @@ -63,7 +63,7 @@ class JmxEndpointIntegrationTests { this.contextRunner.run((context) -> { MBeanServer mBeanServer = context.getBean(MBeanServer.class); checkEndpointMBeans(mBeanServer, new String[] { "health" }, new String[] { "beans", "conditions", - "configprops", "env", "info", "mappings", "threaddump", "httptrace", "shutdown" }); + "configprops", "env", "info", "mappings", "threaddump", "httpexchanges", "shutdown" }); }); } @@ -75,7 +75,7 @@ class JmxEndpointIntegrationTests { .run((context) -> { MBeanServer mBeanServer = context.getBean(MBeanServer.class); checkEndpointMBeans(mBeanServer, new String[] { "beans", "conditions", "configprops", "env", - "health", "info", "mappings", "threaddump", "httptrace" }, new String[] { "shutdown" }); + "health", "info", "mappings", "threaddump", "httpexchanges" }, new String[] { "shutdown" }); }); } @@ -84,7 +84,7 @@ class JmxEndpointIntegrationTests { this.contextRunner.withPropertyValues("management.endpoints.jmx.exposure.exclude:*").run((context) -> { MBeanServer mBeanServer = context.getBean(MBeanServer.class); checkEndpointMBeans(mBeanServer, new String[0], new String[] { "beans", "conditions", "configprops", "env", - "health", "mappings", "shutdown", "threaddump", "httptrace" }); + "health", "mappings", "shutdown", "threaddump", "httpexchanges" }); }); } @@ -94,7 +94,7 @@ class JmxEndpointIntegrationTests { this.contextRunner.withPropertyValues("management.endpoints.jmx.exposure.include=beans").run((context) -> { MBeanServer mBeanServer = context.getBean(MBeanServer.class); checkEndpointMBeans(mBeanServer, new String[] { "beans" }, new String[] { "conditions", "configprops", - "env", "health", "mappings", "shutdown", "threaddump", "httptrace" }); + "env", "health", "mappings", "shutdown", "threaddump", "httpexchanges" }); }); } @@ -144,11 +144,11 @@ class JmxEndpointIntegrationTests { } @Configuration(proxyBeanMethods = false) - static class HttpTraceRepositoryConfiguration { + static class HttpExchangeRepositoryConfiguration { @Bean - InMemoryHttpTraceRepository httpTraceRepository() { - return new InMemoryHttpTraceRepository(); + InMemoryHttpExchangeRepository httpExchangeRepository() { + return new InMemoryHttpExchangeRepository(); } } 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 438cea4669..1df1e5409a 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 @@ -30,13 +30,13 @@ import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; import org.springframework.boot.actuate.endpoint.web.EndpointServlet; import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint; -import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository; +import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; @@ -74,10 +74,10 @@ class WebMvcEndpointExposureIntegrationTests { EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, ManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, ManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, - HttpTraceAutoConfiguration.class, HealthContributorAutoConfiguration.class)) + HttpExchangesAutoConfiguration.class, HealthContributorAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL)) .withUserConfiguration(CustomMvcEndpoint.class, CustomServletEndpoint.class, - HttpTraceRepositoryConfiguration.class, AuditEventRepositoryConfiguration.class) + HttpExchangeRepositoryConfiguration.class, AuditEventRepositoryConfiguration.class) .withPropertyValues("server.port:0"); @Test @@ -95,7 +95,7 @@ class WebMvcEndpointExposureIntegrationTests { assertThat(isExposed(client, HttpMethod.GET, "mappings")).isFalse(); assertThat(isExposed(client, HttpMethod.POST, "shutdown")).isFalse(); assertThat(isExposed(client, HttpMethod.GET, "threaddump")).isFalse(); - assertThat(isExposed(client, HttpMethod.GET, "httptrace")).isFalse(); + assertThat(isExposed(client, HttpMethod.GET, "httpexchanges")).isFalse(); }); } @@ -116,7 +116,7 @@ class WebMvcEndpointExposureIntegrationTests { assertThat(isExposed(client, HttpMethod.GET, "mappings")).isTrue(); assertThat(isExposed(client, HttpMethod.POST, "shutdown")).isFalse(); assertThat(isExposed(client, HttpMethod.GET, "threaddump")).isTrue(); - assertThat(isExposed(client, HttpMethod.GET, "httptrace")).isTrue(); + assertThat(isExposed(client, HttpMethod.GET, "httpexchanges")).isTrue(); }); } @@ -137,7 +137,7 @@ class WebMvcEndpointExposureIntegrationTests { assertThat(isExposed(client, HttpMethod.GET, "mappings")).isFalse(); assertThat(isExposed(client, HttpMethod.POST, "shutdown")).isFalse(); assertThat(isExposed(client, HttpMethod.GET, "threaddump")).isFalse(); - assertThat(isExposed(client, HttpMethod.GET, "httptrace")).isFalse(); + assertThat(isExposed(client, HttpMethod.GET, "httpexchanges")).isFalse(); }); } @@ -158,7 +158,7 @@ class WebMvcEndpointExposureIntegrationTests { assertThat(isExposed(client, HttpMethod.GET, "mappings")).isTrue(); assertThat(isExposed(client, HttpMethod.POST, "shutdown")).isFalse(); assertThat(isExposed(client, HttpMethod.GET, "threaddump")).isTrue(); - assertThat(isExposed(client, HttpMethod.GET, "httptrace")).isTrue(); + assertThat(isExposed(client, HttpMethod.GET, "httpexchanges")).isTrue(); }); } @@ -212,11 +212,11 @@ class WebMvcEndpointExposureIntegrationTests { } @Configuration(proxyBeanMethods = false) - static class HttpTraceRepositoryConfiguration { + static class HttpExchangeRepositoryConfiguration { @Bean - InMemoryHttpTraceRepository httpTraceRepository() { - return new InMemoryHttpTraceRepository(); + InMemoryHttpExchangeRepository httpExchangeRepository() { + return new InMemoryHttpExchangeRepository(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesAutoConfigurationTests.java new file mode 100644 index 0000000000..8ca4da2257 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesAutoConfigurationTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.exchanges; + +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.web.exchanges.HttpExchange; +import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository; +import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository; +import org.springframework.boot.actuate.web.exchanges.Include; +import org.springframework.boot.actuate.web.exchanges.reactive.HttpExchangesWebFilter; +import org.springframework.boot.actuate.web.exchanges.servlet.HttpExchangesFilter; +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 HttpExchangesAutoConfiguration}. + * + * @author Andy Wilkinson + * @author Madhura Bhave + */ +class HttpExchangesAutoConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HttpExchangesAutoConfiguration.class)); + + @Test + void autoConfigurationIsDisabledByDefault() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(HttpExchangesAutoConfiguration.class)); + } + + @Test + void autoConfigurationIsEnabledWhenHttpExchangeRepositoryBeanPresent() { + this.contextRunner.withUserConfiguration(CustomHttpExchangesRepositoryConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(HttpExchangesFilter.class); + assertThat(context).hasSingleBean(HttpExchangeRepository.class); + assertThat(context.getBean(HttpExchangeRepository.class)).isInstanceOf(CustomHttpExchangesRepository.class); + }); + } + + @Test + void usesUserProvidedWebFilterWhenReactiveContext() { + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HttpExchangesAutoConfiguration.class)) + .withUserConfiguration(CustomHttpExchangesRepositoryConfiguration.class) + .withUserConfiguration(CustomWebFilterConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(HttpExchangesWebFilter.class); + assertThat(context.getBean(HttpExchangesWebFilter.class)) + .isInstanceOf(CustomHttpExchangesWebFilter.class); + }); + } + + @Test + void configuresServletFilter() { + this.contextRunner.withUserConfiguration(CustomHttpExchangesRepositoryConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(HttpExchangesFilter.class)); + } + + @Test + void usesUserProvidedServletFilter() { + this.contextRunner.withUserConfiguration(CustomHttpExchangesRepositoryConfiguration.class) + .withUserConfiguration(CustomFilterConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(HttpExchangesFilter.class); + assertThat(context.getBean(HttpExchangesFilter.class)) + .isInstanceOf(CustomHttpExchangesFilter.class); + }); + } + + @Test + void backsOffWhenNotRecording() { + this.contextRunner.withUserConfiguration(CustomHttpExchangesRepositoryConfiguration.class) + .withPropertyValues("management.httpexchanges.record=false") + .run((context) -> assertThat(context).doesNotHaveBean(InMemoryHttpExchangeRepository.class) + .doesNotHaveBean(HttpExchangesFilter.class)); + } + + static class CustomHttpExchangesRepository implements HttpExchangeRepository { + + @Override + public List findAll() { + return null; + } + + @Override + public void add(HttpExchange trace) { + + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomHttpExchangesRepositoryConfiguration { + + @Bean + CustomHttpExchangesRepository customRepository() { + return new CustomHttpExchangesRepository(); + } + + } + + private static final class CustomHttpExchangesWebFilter extends HttpExchangesWebFilter { + + private CustomHttpExchangesWebFilter(HttpExchangeRepository repository, Set includes) { + super(repository, includes); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomWebFilterConfiguration { + + @Bean + CustomHttpExchangesWebFilter customWebFilter(HttpExchangeRepository repository, + HttpExchangesProperties properties) { + return new CustomHttpExchangesWebFilter(repository, properties.getInclude()); + } + + } + + private static final class CustomHttpExchangesFilter extends HttpExchangesFilter { + + private CustomHttpExchangesFilter(HttpExchangeRepository repository, Set includes) { + super(repository, includes); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomFilterConfiguration { + + @Bean + CustomHttpExchangesFilter customWebFilter(HttpExchangeRepository repository, Set includes) { + return new CustomHttpExchangesFilter(repository, includes); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/HttpTraceEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesEndpointAutoConfigurationTests.java similarity index 51% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/HttpTraceEndpointAutoConfigurationTests.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesEndpointAutoConfigurationTests.java index 7fc77d2f4d..98a692e2a9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/HttpTraceEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesEndpointAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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,12 @@ * limitations under the License. */ -package org.springframework.boot.actuate.autoconfigure.web.trace; +package org.springframework.boot.actuate.autoconfigure.web.exchanges; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceEndpointAutoConfiguration; -import org.springframework.boot.actuate.trace.http.HttpTraceEndpoint; -import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository; +import org.springframework.boot.actuate.web.exchanges.HttpExchangesEndpoint; +import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -30,49 +28,49 @@ import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link HttpTraceEndpointAutoConfiguration}. + * Tests for {@link HttpExchangesEndpointAutoConfiguration}. * * @author Phillip Webb * @author Madhura Bhave */ -class HttpTraceEndpointAutoConfigurationTests { +class HttpExchangesEndpointAutoConfigurationTests { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration( - AutoConfigurations.of(HttpTraceAutoConfiguration.class, HttpTraceEndpointAutoConfiguration.class)); + AutoConfigurations.of(HttpExchangesAutoConfiguration.class, HttpExchangesEndpointAutoConfiguration.class)); @Test void runWhenRepositoryBeanAvailableShouldHaveEndpointBean() { - this.contextRunner.withUserConfiguration(HttpTraceRepositoryConfiguration.class) - .withPropertyValues("management.endpoints.web.exposure.include=httptrace") - .run((context) -> assertThat(context).hasSingleBean(HttpTraceEndpoint.class)); + this.contextRunner.withUserConfiguration(HttpExchangeRepositoryConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=httpexchanges") + .run((context) -> assertThat(context).hasSingleBean(HttpExchangesEndpoint.class)); } @Test void runWhenNotExposedShouldNotHaveEndpointBean() { - this.contextRunner.withUserConfiguration(HttpTraceRepositoryConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean(HttpTraceEndpoint.class)); + this.contextRunner.withUserConfiguration(HttpExchangeRepositoryConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(HttpExchangesEndpoint.class)); } @Test void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() { - this.contextRunner.withUserConfiguration(HttpTraceRepositoryConfiguration.class) - .withPropertyValues("management.endpoints.web.exposure.include=httptrace") - .withPropertyValues("management.endpoint.httptrace.enabled:false") - .run((context) -> assertThat(context).doesNotHaveBean(HttpTraceEndpoint.class)); + this.contextRunner.withUserConfiguration(HttpExchangeRepositoryConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=httpexchanges") + .withPropertyValues("management.endpoint.httpexchanges.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(HttpExchangesEndpoint.class)); } @Test void endpointBacksOffWhenRepositoryIsNotAvailable() { - this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=httptrace") - .run((context) -> assertThat(context).doesNotHaveBean(HttpTraceEndpoint.class)); + this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=httpexchanges") + .run((context) -> assertThat(context).doesNotHaveBean(HttpExchangesEndpoint.class)); } @Configuration(proxyBeanMethods = false) - static class HttpTraceRepositoryConfiguration { + static class HttpExchangeRepositoryConfiguration { @Bean - InMemoryHttpTraceRepository customRepository() { - return new InMemoryHttpTraceRepository(); + InMemoryHttpExchangeRepository customHttpExchangeRepository() { + return new InMemoryHttpExchangeRepository(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/HttpTraceAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/HttpTraceAutoConfigurationTests.java deleted file mode 100644 index bef1caa34d..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/HttpTraceAutoConfigurationTests.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.web.trace; - -import java.util.List; -import java.util.Set; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceProperties; -import org.springframework.boot.actuate.trace.http.HttpExchangeTracer; -import org.springframework.boot.actuate.trace.http.HttpTrace; -import org.springframework.boot.actuate.trace.http.HttpTraceRepository; -import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository; -import org.springframework.boot.actuate.trace.http.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 HttpTraceAutoConfiguration}. - * - * @author Andy Wilkinson - * @author Madhura Bhave - */ -class HttpTraceAutoConfigurationTests { - - private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(HttpTraceAutoConfiguration.class)); - - @Test - void autoConfigurationIsDisabledByDefault() { - this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(HttpTraceAutoConfiguration.class)); - } - - @Test - void autoConfigurationIsEnabledWhenHttpTraceRepositoryBeanPresent() { - this.contextRunner.withUserConfiguration(HttpTraceRepositoryConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(HttpExchangeTracer.class); - assertThat(context).hasSingleBean(HttpTraceFilter.class); - assertThat(context).hasSingleBean(HttpTraceRepository.class); - assertThat(context.getBean(HttpTraceRepository.class)).isInstanceOf(CustomHttpTraceRepository.class); - }); - } - - @Test - void usesUserProvidedTracer() { - this.contextRunner.withUserConfiguration(HttpTraceRepositoryConfiguration.class) - .withUserConfiguration(CustomTracerConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(HttpExchangeTracer.class); - assertThat(context.getBean(HttpExchangeTracer.class)).isInstanceOf(CustomHttpExchangeTracer.class); - }); - } - - @Test - void usesUserProvidedWebFilterWhenReactiveContext() { - new ReactiveWebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(HttpTraceAutoConfiguration.class)) - .withUserConfiguration(HttpTraceRepositoryConfiguration.class) - .withUserConfiguration(CustomWebFilterConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(HttpTraceWebFilter.class); - assertThat(context.getBean(HttpTraceWebFilter.class)).isInstanceOf(CustomHttpTraceWebFilter.class); - }); - } - - @Test - void configuresServletFilter() { - this.contextRunner.withUserConfiguration(HttpTraceRepositoryConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(HttpTraceFilter.class)); - } - - @Test - void usesUserProvidedServletFilter() { - this.contextRunner.withUserConfiguration(HttpTraceRepositoryConfiguration.class) - .withUserConfiguration(CustomFilterConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(HttpTraceFilter.class); - assertThat(context.getBean(HttpTraceFilter.class)).isInstanceOf(CustomHttpTraceFilter.class); - }); - } - - @Test - void backsOffWhenDisabled() { - this.contextRunner.withUserConfiguration(HttpTraceRepositoryConfiguration.class) - .withPropertyValues("management.trace.http.enabled=false") - .run((context) -> assertThat(context).doesNotHaveBean(InMemoryHttpTraceRepository.class) - .doesNotHaveBean(HttpExchangeTracer.class).doesNotHaveBean(HttpTraceFilter.class)); - } - - static class CustomHttpTraceRepository implements HttpTraceRepository { - - @Override - public List findAll() { - return null; - } - - @Override - public void add(HttpTrace trace) { - - } - - } - - @Configuration(proxyBeanMethods = false) - static class HttpTraceRepositoryConfiguration { - - @Bean - CustomHttpTraceRepository customRepository() { - return new CustomHttpTraceRepository(); - } - - } - - private static final class CustomHttpExchangeTracer extends HttpExchangeTracer { - - private CustomHttpExchangeTracer(Set includes) { - super(includes); - } - - } - - @Configuration(proxyBeanMethods = false) - static class CustomTracerConfiguration { - - @Bean - CustomHttpExchangeTracer customTracer(HttpTraceProperties 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(proxyBeanMethods = false) - static class CustomWebFilterConfiguration { - - @Bean - CustomHttpTraceWebFilter customWebFilter(HttpTraceRepository repository, HttpExchangeTracer tracer, - HttpTraceProperties 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(proxyBeanMethods = false) - static class CustomFilterConfiguration { - - @Bean - CustomHttpTraceFilter customWebFilter(HttpTraceRepository repository, HttpExchangeTracer tracer) { - return new CustomHttpTraceFilter(repository, tracer); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracer.java deleted file mode 100644 index 97917c0cf5..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracer.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.trace.http; - -import java.net.URI; -import java.security.Principal; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -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 that 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, () -> calculateTimeTaken(trace), 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<>(); - } - return headersSupplier.get().entrySet().stream().filter((entry) -> headerPredicate.test(entry.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - private long calculateTimeTaken(HttpTrace trace) { - return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - trace.getStartNanoTime()); - } - - 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() { - Map> headers = getHeadersIfIncluded(Include.REQUEST_HEADERS, this.delegate::getHeaders, - this::includedHeader); - postProcessRequestHeaders(headers); - return headers; - } - - private boolean includedHeader(String 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, this::includedHeader); - } - - private boolean includedHeader(String 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/trace/http/HttpTrace.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpTrace.java deleted file mode 100644 index 1341ae9fab..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpTrace.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.trace.http; - -import java.net.URI; -import java.time.Instant; -import java.util.LinkedHashMap; -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; - - private volatile Principal principal; - - private volatile Session session; - - private final Request request; - - private volatile Response response; - - private volatile Long timeTaken; - - private final long startNanoTime; - - /** - * Creates a fully-configured {@code HttpTrace} instance. Primarily for use by - * {@link HttpTraceRepository} implementations when recreating a trace from a - * persistent store. - * @param request the request - * @param response the response - * @param timestamp the timestamp of the request-response exchange - * @param principal the principal, if any - * @param session the session, if any - * @param timeTaken the time taken, in milliseconds, to complete the request-response - * exchange, if known - * @since 2.1.0 - */ - public HttpTrace(Request request, Response response, Instant timestamp, Principal principal, Session session, - Long timeTaken) { - this.request = request; - this.response = response; - this.timestamp = timestamp; - this.principal = principal; - this.session = session; - this.timeTaken = timeTaken; - this.startNanoTime = 0; - } - - HttpTrace(TraceableRequest request) { - this.request = new Request(request); - this.timestamp = Instant.now(); - this.startNanoTime = System.nanoTime(); - } - - 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; - } - - long getStartNanoTime() { - return this.startNanoTime; - } - - /** - * 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(request.getMethod(), request.getUri(), request.getHeaders(), request.getRemoteAddress()); - } - - /** - * Creates a fully-configured {@code Request} instance. Primarily for use by - * {@link HttpTraceRepository} implementations when recreating a request from a - * persistent store. - * @param method the HTTP method of the request - * @param uri the URI of the request - * @param headers the request headers - * @param remoteAddress remote address from which the request was sent, if known - * @since 2.1.0 - */ - public Request(String method, URI uri, Map> headers, String remoteAddress) { - this.method = method; - this.uri = uri; - this.headers = new LinkedHashMap<>(headers); - this.remoteAddress = remoteAddress; - } - - 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(response.getStatus(), response.getHeaders()); - } - - /** - * Creates a fully-configured {@code Response} instance. Primarily for use by - * {@link HttpTraceRepository} implementations when recreating a response from a - * persistent store. - * @param status the status of the response - * @param headers the response headers - * @since 2.1.0 - */ - public Response(int status, Map> headers) { - this.status = status; - this.headers = new LinkedHashMap<>(headers); - } - - 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; - - /** - * Creates a {@code Session}. - * @param id the session id - * @since 2.1.0 - */ - public 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; - - /** - * Creates a {@code Principal}. - * @param name the name of the principal - * @since 2.1.0 - */ - public 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/web/exchanges/HttpExchange.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchange.java new file mode 100644 index 0000000000..366b71ebf6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchange.java @@ -0,0 +1,458 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.web.exchanges; + +import java.net.URI; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.springframework.http.HttpHeaders; + +/** + * An HTTP request and response exchange. Can be used for analyzing contextual information + * such as HTTP headers. Data from this class will be exposed by the + * {@link HttpExchangesEndpoint}, usually as JSON. + * + * @author Dave Syer + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.0.0 + */ +public final class HttpExchange { + + private final Instant timestamp; + + private final Request request; + + private final Response response; + + private final Principal principal; + + private final Session session; + + private final Duration timeTaken; + + /** + * Primarily for use by {@link HttpExchangeRepository} implementations when recreating + * an exchange from a persistent store. + * @param timestamp the instant that the exchange started + * @param request the request + * @param response the response + * @param principal the principal + * @param session the session + * @param timeTaken the total time taken + */ + public HttpExchange(Instant timestamp, Request request, Response response, Principal principal, Session session, + Duration timeTaken) { + this.timestamp = timestamp; + this.request = request; + this.response = response; + this.principal = principal; + this.session = session; + this.timeTaken = timeTaken; + } + + /** + * Returns the instant that the exchange started. + * @return the start timestamp + */ + public Instant getTimestamp() { + return this.timestamp; + } + + /** + * Returns the request that started the exchange. + * @return the request. + */ + public Request getRequest() { + return this.request; + } + + /** + * Returns the response that completed the exchange. + * @return the response. + */ + public Response getResponse() { + return this.response; + } + + /** + * Returns the principal. + * @return the the request + */ + public Principal getPrincipal() { + return this.principal; + } + + /** + * Returns the session details. + * @return the session + */ + public Session getSession() { + return this.session; + } + + /** + * Returns the total time taken for the exchange. + * @return the total time taken + */ + public Duration getTimeTaken() { + return this.timeTaken; + } + + /** + * Start a new {@link Started} from the given source request. + * @param sourceHttpRequest the source HTTP request + * @return an in-progress request + */ + public static Started start(SourceHttpRequest sourceHttpRequest) { + return start(Clock.systemUTC(), sourceHttpRequest); + } + + /** + * Start a new {@link Started} from the given source request. + * @param clock the clock to use + * @param sourceHttpRequest the source HTTP request + * @return an in-progress request + */ + public static Started start(Clock clock, SourceHttpRequest sourceHttpRequest) { + return new Started(clock, sourceHttpRequest); + } + + /** + * A started request that when {@link #finish finished} will return a new + * {@link HttpExchange} instance. + */ + public static final class Started { + + private final Instant timestamp; + + private final SourceHttpRequest sourceRequest; + + private Started(Clock clock, SourceHttpRequest sourceRequest) { + this.timestamp = Instant.now(clock); + this.sourceRequest = sourceRequest; + } + + /** + * Finish the request and return a new {@link HttpExchange} instance. + * @param sourceHttpResponse the source HTTP response + * @param principalSupplier a supplier to provide the principal + * @param sessionIdSupplier a supplier to provide the session ID + * @param includes the options to include + * @return a new {@link HttpExchange} instance + */ + public HttpExchange finish(SourceHttpResponse sourceHttpResponse, + Supplier principalSupplier, Supplier sessionIdSupplier, + Include... includes) { + return finish(Clock.systemUTC(), sourceHttpResponse, principalSupplier, sessionIdSupplier, includes); + } + + /** + * Finish the request and return a new {@link HttpExchange} instance. + * @param clock the clock to use + * @param sourceHttpResponse the source HTTP response + * @param principalSupplier a supplier to provide the principal + * @param sessionIdSupplier a supplier to provide the session ID + * @param includes the options to include + * @return a new {@link HttpExchange} instance + */ + public HttpExchange finish(Clock clock, SourceHttpResponse sourceHttpResponse, + Supplier principalSupplier, Supplier sessionIdSupplier, + Include... includes) { + return finish(clock, sourceHttpResponse, principalSupplier, sessionIdSupplier, + new HashSet<>(Arrays.asList(includes))); + } + + /** + * Finish the request and return a new {@link HttpExchange} instance. + * @param sourceHttpResponse the source HTTP response + * @param principalSupplier a supplier to provide the principal + * @param sessionIdSupplier a supplier to provide the session ID + * @param includes the options to include + * @return a new {@link HttpExchange} instance + */ + public HttpExchange finish(SourceHttpResponse sourceHttpResponse, + Supplier principalSupplier, Supplier sessionIdSupplier, + Set includes) { + return finish(Clock.systemUTC(), sourceHttpResponse, principalSupplier, sessionIdSupplier, includes); + } + + /** + * Finish the request and return a new {@link HttpExchange} instance. + * @param clock the clock to use + * @param sourceHttpResponse the source HTTP response + * @param principalSupplier a supplier to provide the principal + * @param sessionIdSupplier a supplier to provide the session ID + * @param includes the options to include + * @return a new {@link HttpExchange} instance + */ + public HttpExchange finish(Clock clock, SourceHttpResponse sourceHttpResponse, + Supplier principalSupplier, Supplier sessionIdSupplier, + Set includes) { + Request request = new Request(this.sourceRequest, includes); + Response response = new Response(sourceHttpResponse, includes); + Principal principal = getIfIncluded(includes, Include.PRINCIPAL, () -> Principal.from(principalSupplier)); + Session session = getIfIncluded(includes, Include.SESSION_ID, () -> Session.from(sessionIdSupplier)); + Duration duration = getIfIncluded(includes, Include.TIME_TAKEN, + () -> Duration.between(this.timestamp, Instant.now(clock))); + return new HttpExchange(this.timestamp, request, response, principal, session, duration); + } + + private T getIfIncluded(Set includes, Include include, Supplier supplier) { + return (includes.contains(include)) ? supplier.get() : null; + } + + } + + /** + * The request that started the exchange. + */ + public static final class Request { + + private final URI uri; + + private final String remoteAddress; + + private final String method; + + private final Map> headers; + + private Request(SourceHttpRequest source, Set includes) { + this.uri = source.getUri(); + this.remoteAddress = (includes.contains(Include.REMOTE_ADDRESS)) ? source.getRemoteAddress() : null; + this.method = source.getMethod(); + this.headers = Collections.unmodifiableMap(filterHeaders(source.getHeaders(), includes)); + } + + /** + * Creates a fully-configured {@code Request} instance. Primarily for use by + * {@link HttpExchangeRepository} implementations when recreating a request from a + * persistent store. + * @param uri the URI of the request + * @param remoteAddress remote address from which the request was sent, if known + * @param method the HTTP method of the request + * @param headers the request headers + */ + public Request(URI uri, String remoteAddress, String method, Map> headers) { + this.uri = uri; + this.remoteAddress = remoteAddress; + this.method = method; + this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers)); + } + + private Map> filterHeaders(Map> headers, Set includes) { + HeadersFilter filter = new HeadersFilter(includes, Include.REQUEST_HEADERS); + filter.excludeUnless(HttpHeaders.COOKIE, Include.COOKIE_HEADERS); + filter.excludeUnless(HttpHeaders.AUTHORIZATION, Include.AUTHORIZATION_HEADER); + return filter.apply(headers); + } + + /** + * Return the HTTP method requested. + * @return the HTTP method + */ + public String getMethod() { + return this.method; + } + + /** + * Return the URI requested. + * @return the URI + */ + public URI getUri() { + return this.uri; + } + + /** + * Return the request headers. + * @return the request headers + */ + public Map> getHeaders() { + return this.headers; + } + + /** + * Return the remote address that made the request. + * @return the remote address + */ + public String getRemoteAddress() { + return this.remoteAddress; + } + + } + + /** + * The response that finished the exchange. + */ + public static final class Response { + + private final int status; + + private final Map> headers; + + private Response(SourceHttpResponse source, Set includes) { + this.status = source.getStatus(); + this.headers = Collections.unmodifiableMap(filterHeaders(source.getHeaders(), includes)); + } + + /** + * Creates a fully-configured {@code Response} instance. Primarily for use by + * {@link HttpExchangeRepository} implementations when recreating a response from + * a persistent store. + * @param status the status of the response + * @param headers the response headers + */ + public Response(int status, Map> headers) { + this.status = status; + this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers)); + } + + private Map> filterHeaders(Map> headers, Set includes) { + HeadersFilter filter = new HeadersFilter(includes, Include.RESPONSE_HEADERS); + filter.excludeUnless(HttpHeaders.SET_COOKIE, Include.COOKIE_HEADERS); + return filter.apply(headers); + } + + /** + * Return the status code of the response. + * @return the response status code + */ + public int getStatus() { + return this.status; + } + + /** + * Return the response headers. + * @return the headers + */ + public Map> getHeaders() { + return this.headers; + } + + } + + /** + * The session associated with the exchange. + */ + public static final class Session { + + private final String id; + + /** + * Creates a {@code Session}. Primarily for use by {@link HttpExchangeRepository} + * implementations when recreating a session from a persistent store. + * @param id the session id + */ + public Session(String id) { + this.id = id; + } + + /** + * Return the ID of the session. + * @return the session ID + */ + public String getId() { + return this.id; + } + + static Session from(Supplier sessionIdSupplier) { + String id = sessionIdSupplier.get(); + return (id != null) ? new Session(id) : null; + } + + } + + /** + * Principal associated with an HTTP request-response exchange. + */ + public static final class Principal { + + private final String name; + + /** + * Creates a {@code Principal}. Primarily for use by {@link Principal} + * implementations when recreating a response from a persistent store. + * @param name the name of the principal + */ + public Principal(String name) { + this.name = name; + } + + /** + * Return the name of the principal. + * @return the principal name + */ + public String getName() { + return this.name; + } + + static Principal from(Supplier principalSupplier) { + java.security.Principal principal = principalSupplier.get(); + return (principal != null) ? new Principal(principal.getName()) : null; + } + + } + + /** + * Utility class used to filter headers. + */ + private static class HeadersFilter { + + private final Set includes; + + private final Include requiredInclude; + + private final Set filteredHeaderNames; + + HeadersFilter(Set includes, Include requiredInclude) { + this.includes = includes; + this.requiredInclude = requiredInclude; + this.filteredHeaderNames = new HashSet<>(); + } + + void excludeUnless(String header, Include exception) { + if (!this.includes.contains(exception)) { + this.filteredHeaderNames.add(header.toLowerCase()); + } + } + + Map> apply(Map> headers) { + if (!this.includes.contains(this.requiredInclude)) { + return Collections.emptyMap(); + } + Map> filtered = new LinkedHashMap<>(); + headers.forEach((name, value) -> { + if (!this.filteredHeaderNames.contains(name.toLowerCase())) { + filtered.put(name, value); + } + }); + return filtered; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpTraceRepository.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchangeRepository.java similarity index 56% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpTraceRepository.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchangeRepository.java index b26a9a90bc..43e2ae8b9b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpTraceRepository.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchangeRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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,29 +14,29 @@ * limitations under the License. */ -package org.springframework.boot.actuate.trace.http; +package org.springframework.boot.actuate.web.exchanges; import java.util.List; /** - * A repository for {@link HttpTrace}s. + * A repository for {@link HttpExchange} instances. * * @author Dave Syer * @author Andy Wilkinson - * @since 2.0.0 + * @since 3.0.0 */ -public interface HttpTraceRepository { +public interface HttpExchangeRepository { /** - * Find all {@link HttpTrace} objects contained in the repository. - * @return the results + * Find all {@link HttpExchange} instances contained in the repository. + * @return all contained HTTP exchanges */ - List findAll(); + List findAll(); /** - * Adds a trace to the repository. - * @param trace the trace to add + * Adds an {@link HttpExchange} instance to the repository. + * @param httpExchange the HTTP exchange to add */ - void add(HttpTrace trace); + void add(HttpExchange httpExchange); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpTraceEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchangesEndpoint.java similarity index 53% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpTraceEndpoint.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchangesEndpoint.java index 931e87f373..7445ad1235 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpTraceEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchangesEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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.http; +package org.springframework.boot.actuate.web.exchanges; import java.util.List; @@ -23,45 +23,45 @@ import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.util.Assert; /** - * {@link Endpoint @Endpoint} to expose {@link HttpTrace} information. + * {@link Endpoint @Endpoint} to expose {@link HttpExchange} information. * * @author Dave Syer * @author Andy Wilkinson - * @since 2.0.0 + * @since 3.0.0 */ -@Endpoint(id = "httptrace") -public class HttpTraceEndpoint { +@Endpoint(id = "httpexchanges") +public class HttpExchangesEndpoint { - private final HttpTraceRepository repository; + private final HttpExchangeRepository repository; /** - * Create a new {@link HttpTraceEndpoint} instance. + * Create a new {@link HttpExchangesEndpoint} instance. * @param repository the trace repository */ - public HttpTraceEndpoint(HttpTraceRepository repository) { + public HttpExchangesEndpoint(HttpExchangeRepository repository) { Assert.notNull(repository, "Repository must not be null"); this.repository = repository; } @ReadOperation - public HttpTraceDescriptor traces() { - return new HttpTraceDescriptor(this.repository.findAll()); + public HttpExchanges httpExchanges() { + return new HttpExchanges(this.repository.findAll()); } /** - * A description of an application's {@link HttpTrace} entries. Primarily intended for - * serialization to JSON. + * A description of an application's {@link HttpExchange} entries. Primarily intended + * for serialization to JSON. */ - public static final class HttpTraceDescriptor { + public static final class HttpExchanges { - private final List traces; + private final List exchanges; - private HttpTraceDescriptor(List traces) { - this.traces = traces; + private HttpExchanges(List exchanges) { + this.exchanges = exchanges; } - public List getTraces() { - return this.traces; + public List getExchanges() { + return this.exchanges; } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/InMemoryHttpTraceRepository.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/InMemoryHttpExchangeRepository.java similarity index 59% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/InMemoryHttpTraceRepository.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/InMemoryHttpExchangeRepository.java index 3f9e6adaf1..b1d4831915 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/InMemoryHttpTraceRepository.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/InMemoryHttpExchangeRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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.http; +package org.springframework.boot.actuate.web.exchanges; import java.util.ArrayList; import java.util.Collections; @@ -22,26 +22,26 @@ import java.util.LinkedList; import java.util.List; /** - * In-memory implementation of {@link HttpTraceRepository}. + * In-memory implementation of {@link HttpExchangeRepository}. * * @author Dave Syer * @author Olivier Bourgain - * @since 2.0.0 + * @since 3.0.0 */ -public class InMemoryHttpTraceRepository implements HttpTraceRepository { +public class InMemoryHttpExchangeRepository implements HttpExchangeRepository { private int capacity = 100; private boolean reverse = true; - private final List traces = new LinkedList<>(); + private final List httpExchanges = new LinkedList<>(); /** * Flag to say that the repository lists traces in reverse order. * @param reverse flag value (default true) */ public void setReverse(boolean reverse) { - synchronized (this.traces) { + synchronized (this.httpExchanges) { this.reverse = reverse; } } @@ -51,29 +51,29 @@ public class InMemoryHttpTraceRepository implements HttpTraceRepository { * @param capacity the capacity */ public void setCapacity(int capacity) { - synchronized (this.traces) { + synchronized (this.httpExchanges) { this.capacity = capacity; } } @Override - public List findAll() { - synchronized (this.traces) { - return Collections.unmodifiableList(new ArrayList<>(this.traces)); + public List findAll() { + synchronized (this.httpExchanges) { + return Collections.unmodifiableList(new ArrayList<>(this.httpExchanges)); } } @Override - public void add(HttpTrace trace) { - synchronized (this.traces) { - while (this.traces.size() >= this.capacity) { - this.traces.remove(this.reverse ? this.capacity - 1 : 0); + public void add(HttpExchange trace) { + synchronized (this.httpExchanges) { + while (this.httpExchanges.size() >= this.capacity) { + this.httpExchanges.remove(this.reverse ? this.capacity - 1 : 0); } if (this.reverse) { - this.traces.add(0, trace); + this.httpExchanges.add(0, trace); } else { - this.traces.add(trace); + this.httpExchanges.add(trace); } } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/Include.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/Include.java similarity index 86% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/Include.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/Include.java index 86e0ae5d3a..34b65e4961 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/Include.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/Include.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2022 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,19 +14,19 @@ * limitations under the License. */ -package org.springframework.boot.actuate.trace.http; +package org.springframework.boot.actuate.web.exchanges; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; /** - * Include options for HTTP tracing. + * Include options for HTTP exchanges. * * @author Wallace Wadge * @author Emily Tsanova * @author Joseph Beeton - * @since 2.0.0 + * @since 3.0.0 */ public enum Include { @@ -36,9 +36,9 @@ public enum Include { REQUEST_HEADERS, /** - * Include response headers. + * Include the remote address from the request. */ - RESPONSE_HEADERS, + REMOTE_ADDRESS, /** * Include "Cookie" header (if any) in request headers and "Set-Cookie" (if any) in @@ -52,14 +52,14 @@ public enum Include { AUTHORIZATION_HEADER, /** - * Include the principal. + * Include response headers. */ - PRINCIPAL, + RESPONSE_HEADERS, /** - * Include the remote address. + * Include the principal. */ - REMOTE_ADDRESS, + PRINCIPAL, /** * Include the session ID. @@ -67,7 +67,7 @@ public enum Include { SESSION_ID, /** - * Include the time taken to service the request in milliseconds. + * Include the time taken to service the request. */ TIME_TAKEN; @@ -75,9 +75,9 @@ public enum Include { static { Set defaultIncludes = new LinkedHashSet<>(); + defaultIncludes.add(Include.TIME_TAKEN); defaultIncludes.add(Include.REQUEST_HEADERS); defaultIncludes.add(Include.RESPONSE_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/trace/http/TraceableRequest.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/SourceHttpRequest.java similarity index 79% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/TraceableRequest.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/SourceHttpRequest.java index 4dbe55a23c..1dfc6fd737 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/TraceableRequest.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/SourceHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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,26 +14,21 @@ * limitations under the License. */ -package org.springframework.boot.actuate.trace.http; +package org.springframework.boot.actuate.web.exchanges; import java.net.URI; import java.util.List; import java.util.Map; /** - * A representation of an HTTP request that is suitable for tracing. + * The source of an HTTP request that will result in an {@link HttpExchange}. * * @author Andy Wilkinson - * @since 2.0.0 - * @see HttpExchangeTracer + * @author Phillip Webb + * @since 3.0.0 + * @see SourceHttpResponse */ -public interface TraceableRequest { - - /** - * Returns the method (GET, POST, etc.) of the request. - * @return the method - */ - String getMethod(); +public interface SourceHttpRequest { /** * Returns the URI of the request. @@ -41,16 +36,22 @@ public interface TraceableRequest { */ 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(); + /** + * Returns the method (GET, POST, etc.) of the request. + * @return the method + */ + String getMethod(); + + /** + * Returns a modifiable copy of the headers of the request. + * @return the headers + */ + Map> getHeaders(); + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/TraceableResponse.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/SourceHttpResponse.java similarity index 76% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/TraceableResponse.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/SourceHttpResponse.java index b6f6d02a4e..2039d64900 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/TraceableResponse.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/SourceHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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,19 +14,19 @@ * limitations under the License. */ -package org.springframework.boot.actuate.trace.http; +package org.springframework.boot.actuate.web.exchanges; import java.util.List; import java.util.Map; /** - * A representation of an HTTP response that is suitable for tracing. + * The source of an HTTP response that will result in an {@link HttpExchange}. * * @author Andy Wilkinson - * @since 2.0.0 - * @see HttpExchangeTracer + * @since 3.0.0 + * @see SourceHttpRequest */ -public interface TraceableResponse { +public interface SourceHttpResponse { /** * The status of the response. diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/package-info.java similarity index 72% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/package-info.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/package-info.java index 777900d8c8..cce3a7d1f6 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/package-info.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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 HTTP tracing support. + * Actuator HTTP exchanges support. * - * @see org.springframework.boot.actuate.trace.http.HttpTraceRepository + * @see org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository */ -package org.springframework.boot.actuate.trace.http; +package org.springframework.boot.actuate.web.exchanges; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/reactive/HttpExchangesWebFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/reactive/HttpExchangesWebFilter.java new file mode 100644 index 0000000000..3b32be692e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/reactive/HttpExchangesWebFilter.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.web.exchanges.reactive; + +import java.security.Principal; +import java.util.Set; + +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.web.exchanges.HttpExchange; +import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository; +import org.springframework.boot.actuate.web.exchanges.Include; +import org.springframework.core.Ordered; +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 recording {@link HttpExchange HTTP exchanges}. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.0.0 + */ +public class HttpExchangesWebFilter 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 HttpExchangeRepository repository; + + private final Set includes; + + /** + * Create a new {@link HttpExchangesWebFilter} instance. + * @param repository the repository used to record events + * @param includes the include options + */ + public HttpExchangesWebFilter(HttpExchangeRepository repository, Set includes) { + this.repository = repository; + 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 = exchange.getPrincipal().cast(Object.class).defaultIfEmpty(NONE); + Mono session = exchange.getSession().cast(Object.class).defaultIfEmpty(NONE); + return Mono.zip(PrincipalAndSession::new, principal, session) + .flatMap((principalAndSession) -> filter(exchange, chain, principalAndSession)); + } + + private Mono filter(ServerWebExchange exchange, WebFilterChain chain, + PrincipalAndSession principalAndSession) { + return Mono.fromRunnable(() -> addExchangeOnCommit(exchange, principalAndSession)).and(chain.filter(exchange)); + } + + private void addExchangeOnCommit(ServerWebExchange exchange, PrincipalAndSession principalAndSession) { + SourceServerHttpRequest sourceRequest = new SourceServerHttpRequest(exchange.getRequest()); + HttpExchange.Started startedHtppExchange = HttpExchange.start(sourceRequest); + exchange.getResponse().beforeCommit(() -> { + SourceServerHttpResponse sourceResponse = new SourceServerHttpResponse(exchange.getResponse()); + HttpExchange finishedExchange = startedHtppExchange.finish(sourceResponse, + principalAndSession::getPrincipal, principalAndSession::getSessionId, this.includes); + this.repository.add(finishedExchange); + return Mono.empty(); + }); + } + + /** + * A {@link Principal} and {@link WebSession}. + */ + private static class PrincipalAndSession { + + private final Principal principal; + + private final WebSession session; + + PrincipalAndSession(Object[] zipped) { + this.principal = (zipped[0] != NONE) ? (Principal) zipped[0] : null; + this.session = (zipped[1] != NONE) ? (WebSession) zipped[1] : null; + } + + Principal getPrincipal() { + return this.principal; + } + + String getSessionId() { + return (this.session != null && this.session.isStarted()) ? this.session.getId() : null; + } + + } + +} 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/exchanges/reactive/SourceServerHttpRequest.java similarity index 80% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequest.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/reactive/SourceServerHttpRequest.java index c6eeeb9746..7079f3229e 100644 --- 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/exchanges/reactive/SourceServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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.web.trace.reactive; +package org.springframework.boot.actuate.web.exchanges.reactive; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -23,16 +23,16 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.springframework.boot.actuate.trace.http.TraceableRequest; +import org.springframework.boot.actuate.web.exchanges.SourceHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; /** - * A {@link TraceableRequest} backed by a {@link ServerWebExchange}. + * A {@link SourceHttpRequest} backed by a {@link ServerWebExchange}. * * @author Andy Wilkinson */ -class ServerWebExchangeTraceableRequest implements TraceableRequest { +class SourceServerHttpRequest implements SourceHttpRequest { private final String method; @@ -42,8 +42,7 @@ class ServerWebExchangeTraceableRequest implements TraceableRequest { private final String remoteAddress; - ServerWebExchangeTraceableRequest(ServerWebExchange exchange) { - ServerHttpRequest request = exchange.getRequest(); + SourceServerHttpRequest(ServerHttpRequest request) { this.method = request.getMethod().name(); this.headers = request.getHeaders(); this.uri = request.getURI(); 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/exchanges/reactive/SourceServerHttpResponse.java similarity index 78% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/TraceableServerHttpResponse.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/reactive/SourceServerHttpResponse.java index 8a3b2c81b0..462ffd3322 100644 --- 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/exchanges/reactive/SourceServerHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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,28 @@ * limitations under the License. */ -package org.springframework.boot.actuate.web.trace.reactive; +package org.springframework.boot.actuate.web.exchanges.reactive; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.springframework.boot.actuate.trace.http.TraceableResponse; +import org.springframework.boot.actuate.web.exchanges.SourceHttpResponse; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; /** - * An adapter that exposes a {@link ServerHttpResponse} as a {@link TraceableResponse}. + * An adapter that exposes a {@link ServerHttpResponse} as a {@link SourceHttpResponse}. * * @author Andy Wilkinson */ -class TraceableServerHttpResponse implements TraceableResponse { +class SourceServerHttpResponse implements SourceHttpResponse { private final int status; private final Map> headers; - TraceableServerHttpResponse(ServerHttpResponse response) { + SourceServerHttpResponse(ServerHttpResponse response) { this.status = (response.getStatusCode() != null) ? response.getStatusCode().value() : HttpStatus.OK.value(); this.headers = new LinkedHashMap<>(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/exchanges/reactive/package-info.java similarity index 69% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/package-info.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/reactive/package-info.java index 2658510688..650befc80b 100644 --- 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/exchanges/reactive/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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 reactive HTTP tracing support. + * Actuator HTTP exchanges support for reactive servers. * - * @see org.springframework.boot.actuate.trace.http.HttpTraceRepository + * @see org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository */ -package org.springframework.boot.actuate.web.trace.reactive; +package org.springframework.boot.actuate.web.exchanges.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/exchanges/servlet/HttpExchangesFilter.java similarity index 57% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/HttpTraceFilter.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/servlet/HttpExchangesFilter.java index 88772d6d1f..c4e67ae9a2 100644 --- 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/exchanges/servlet/HttpExchangesFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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,55 +14,56 @@ * limitations under the License. */ -package org.springframework.boot.actuate.web.trace.servlet; +package org.springframework.boot.actuate.web.exchanges.servlet; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.Set; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpServletResponseWrapper; import jakarta.servlet.http.HttpSession; -import org.springframework.boot.actuate.trace.http.HttpExchangeTracer; -import org.springframework.boot.actuate.trace.http.HttpTrace; -import org.springframework.boot.actuate.trace.http.HttpTraceRepository; +import org.springframework.boot.actuate.web.exchanges.HttpExchange; +import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository; +import org.springframework.boot.actuate.web.exchanges.Include; +import org.springframework.boot.actuate.web.exchanges.reactive.HttpExchangesWebFilter; 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}. + * Servlet {@link Filter} for recording {@link HttpExchange HTTP exchanges}. * * @author Dave Syer * @author Wallace Wadge * @author Andy Wilkinson * @author Venil Noronha * @author Madhura Bhave - * @since 2.0.0 + * @since 3.0.0 */ -public class HttpTraceFilter extends OncePerRequestFilter implements Ordered { +public class HttpExchangesFilter 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 HttpExchangeRepository repository; - private final HttpExchangeTracer tracer; + private final Set includes; /** - * Create a new {@link HttpTraceFilter} instance. - * @param repository the trace repository - * @param tracer used to trace exchanges + * Create a new {@link HttpExchangesWebFilter} instance. + * @param repository the repository used to record events + * @param includes the include options */ - public HttpTraceFilter(HttpTraceRepository repository, HttpExchangeTracer tracer) { + public HttpExchangesFilter(HttpExchangeRepository repository, Set includes) { this.repository = repository; - this.tracer = tracer; + this.includes = includes; } @Override @@ -81,19 +82,18 @@ public class HttpTraceFilter extends OncePerRequestFilter implements Ordered { filterChain.doFilter(request, response); return; } - TraceableHttpServletRequest traceableRequest = new TraceableHttpServletRequest(request); - HttpTrace trace = this.tracer.receivedRequest(traceableRequest); + ServletSourceHttpRequest sourceRequest = new ServletSourceHttpRequest(request); + HttpExchange.Started startedHtppExchange = HttpExchange.start(sourceRequest); int status = HttpStatus.INTERNAL_SERVER_ERROR.value(); try { filterChain.doFilter(request, response); status = response.getStatus(); } finally { - TraceableHttpServletResponse traceableResponse = new TraceableHttpServletResponse( - (status != response.getStatus()) ? new CustomStatusResponseWrapper(response, status) : response); - this.tracer.sendingResponse(trace, traceableResponse, request::getUserPrincipal, - () -> getSessionId(request)); - this.repository.add(trace); + SourceServletHttpResponse sourceResponse = new SourceServletHttpResponse(response, status); + HttpExchange finishedExchange = startedHtppExchange.finish(sourceResponse, request::getUserPrincipal, + () -> getSessionId(request), this.includes); + this.repository.add(finishedExchange); } } @@ -112,20 +112,4 @@ public class HttpTraceFilter extends OncePerRequestFilter implements Ordered { return (session != null) ? session.getId() : null; } - 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/exchanges/servlet/ServletSourceHttpRequest.java similarity index 87% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletRequest.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/servlet/ServletSourceHttpRequest.java index 9a7e4c9949..24f00c296b 100644 --- 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/exchanges/servlet/ServletSourceHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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.web.trace.servlet; +package org.springframework.boot.actuate.web.exchanges.servlet; import java.net.URI; import java.net.URISyntaxException; @@ -27,20 +27,20 @@ import java.util.Map; import jakarta.servlet.http.HttpServletRequest; -import org.springframework.boot.actuate.trace.http.TraceableRequest; +import org.springframework.boot.actuate.web.exchanges.SourceHttpRequest; import org.springframework.util.StringUtils; import org.springframework.web.util.UriUtils; /** - * An adapter that exposes an {@link HttpServletRequest} as a {@link TraceableRequest}. + * An adapter that exposes an {@link HttpServletRequest} as a {@link SourceHttpRequest}. * * @author Andy Wilkinson */ -final class TraceableHttpServletRequest implements TraceableRequest { +final class ServletSourceHttpRequest implements SourceHttpRequest { private final HttpServletRequest request; - TraceableHttpServletRequest(HttpServletRequest request) { + ServletSourceHttpRequest(HttpServletRequest request) { this.request = request; } 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/exchanges/servlet/SourceServletHttpResponse.java similarity index 72% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletResponse.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/servlet/SourceServletHttpResponse.java index cce56777a1..6b5f2c5d3a 100644 --- 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/exchanges/servlet/SourceServletHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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.web.trace.servlet; +package org.springframework.boot.actuate.web.exchanges.servlet; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -23,32 +23,31 @@ import java.util.Map; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.boot.actuate.trace.http.TraceableResponse; +import org.springframework.boot.actuate.web.exchanges.SourceHttpResponse; /** - * An adapter that exposes an {@link HttpServletResponse} as a {@link TraceableResponse}. + * An adapter that exposes an {@link HttpServletResponse} as a {@link SourceHttpResponse}. * * @author Andy Wilkinson */ -final class TraceableHttpServletResponse implements TraceableResponse { +final class SourceServletHttpResponse implements SourceHttpResponse { private final HttpServletResponse delegate; - TraceableHttpServletResponse(HttpServletResponse response) { + private final int status; + + SourceServletHttpResponse(HttpServletResponse response, int status) { this.delegate = response; + this.status = status; } @Override public int getStatus() { - return this.delegate.getStatus(); + return this.status; } @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))); 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/exchanges/servlet/package-info.java similarity index 69% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/servlet/package-info.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/servlet/package-info.java index a665328537..b3b7ac52e4 100644 --- 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/exchanges/servlet/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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 servlet HTTP tracing support. + * Actuator HTTP exchanges support for servlet servers. * - * @see org.springframework.boot.actuate.trace.http.HttpTraceRepository + * @see org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository */ -package org.springframework.boot.actuate.web.trace.servlet; +package org.springframework.boot.actuate.web.exchanges.servlet; 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 deleted file mode 100644 index 224724f8d7..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/HttpTraceWebFilter.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.web.trace.reactive; - -import java.security.Principal; -import java.util.Set; - -import reactor.core.publisher.Mono; - -import org.springframework.boot.actuate.trace.http.HttpExchangeTracer; -import org.springframework.boot.actuate.trace.http.HttpTrace; -import org.springframework.boot.actuate.trace.http.HttpTraceRepository; -import org.springframework.boot.actuate.trace.http.Include; -import org.springframework.core.Ordered; -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); - exchange.getResponse().beforeCommit(() -> { - TraceableServerHttpResponse response = new TraceableServerHttpResponse(exchange.getResponse()); - this.tracer.sendingResponse(trace, response, () -> principal, () -> getStartedSessionId(session)); - this.repository.add(trace); - return Mono.empty(); - }); - return chain.filter(exchange); - } - - private String getStartedSessionId(WebSession session) { - return (session != null && session.isStarted()) ? session.getId() : null; - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracerTests.java deleted file mode 100644 index 3b8a35fa6c..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracerTests.java +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.trace.http; - -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.jupiter.api.Test; - -import org.springframework.boot.actuate.trace.http.HttpTrace.Request; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -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 - */ -class HttpExchangeTracerTests { - - @Test - void methodIsIncluded() { - HttpTrace trace = new HttpExchangeTracer(EnumSet.noneOf(Include.class)).receivedRequest(createRequest()); - Request request = trace.getRequest(); - assertThat(request.getMethod()).isEqualTo("GET"); - } - - @Test - 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 - void remoteAddressIsNotIncludedByDefault() { - HttpTrace trace = new HttpExchangeTracer(EnumSet.noneOf(Include.class)).receivedRequest(createRequest()); - Request request = trace.getRequest(); - assertThat(request.getRemoteAddress()).isNull(); - } - - @Test - 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 - void requestHeadersAreNotIncludedByDefault() { - HttpTrace trace = new HttpExchangeTracer(EnumSet.noneOf(Include.class)).receivedRequest(createRequest()); - Request request = trace.getRequest(); - assertThat(request.getHeaders()).isEmpty(); - } - - @Test - void requestHeadersCanBeIncluded() { - HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REQUEST_HEADERS)).receivedRequest(createRequest()); - Request request = trace.getRequest(); - assertThat(request.getHeaders()).containsOnlyKeys(HttpHeaders.ACCEPT); - } - - @Test - void requestHeadersCanBeCustomized() { - MultiValueMap headers = new LinkedMultiValueMap<>(); - headers.add("to-remove", "test"); - headers.add("test", "value"); - HttpTrace trace = new RequestHeadersFilterHttpExchangeTracer().receivedRequest(createRequest(headers)); - Request request = trace.getRequest(); - assertThat(request.getHeaders()).containsOnlyKeys("test", "to-add"); - assertThat(request.getHeaders().get("test")).containsExactly("value"); - assertThat(request.getHeaders().get("to-add")).containsExactly("42"); - } - - @Test - 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 - 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 - 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 - 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 - 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 - void mixedCaseCookieHeaderIsNotIncludedByDefault() { - HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REQUEST_HEADERS)).receivedRequest( - createRequest(Collections.singletonMap(mixedCase(HttpHeaders.COOKIE), Arrays.asList("value")))); - Request request = trace.getRequest(); - assertThat(request.getHeaders()).isEmpty(); - } - - @Test - 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 - 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 - 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 - void responseHeadersAreNotIncludedByDefault() { - HttpTrace trace = new HttpTrace(createRequest()); - new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace, createResponse(), null, null); - assertThat(trace.getResponse().getHeaders()).isEmpty(); - } - - @Test - 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 - 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 - 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 - 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 - 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 - void principalIsNotIncludedByDefault() { - HttpTrace trace = new HttpTrace(createRequest()); - new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace, createResponse(), - this::createPrincipal, null); - assertThat(trace.getPrincipal()).isNull(); - } - - @Test - void principalCanBeIncluded() { - HttpTrace trace = new HttpTrace(createRequest()); - new HttpExchangeTracer(EnumSet.of(Include.PRINCIPAL)).sendingResponse(trace, createResponse(), - this::createPrincipal, null); - assertThat(trace.getPrincipal()).isNotNull(); - assertThat(trace.getPrincipal().getName()).isEqualTo("alice"); - } - - @Test - void sessionIdIsNotIncludedByDefault() { - HttpTrace trace = new HttpTrace(createRequest()); - new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace, createResponse(), null, - () -> "sessionId"); - assertThat(trace.getSession()).isNull(); - } - - @Test - 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 - void timeTakenIsNotIncludedByDefault() { - HttpTrace trace = new HttpTrace(createRequest()); - new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace, createResponse(), null, null); - assertThat(trace.getTimeTaken()).isNull(); - } - - @Test - void timeTakenCanBeIncluded() { - HttpTrace trace = new HttpTrace(createRequest()); - new HttpExchangeTracer(EnumSet.of(Include.TIME_TAKEN)).sendingResponse(trace, createResponse(), null, null); - assertThat(trace.getTimeTaken()).isNotNull(); - } - - @Test - void defaultIncludes() { - HttpHeaders requestHeaders = new HttpHeaders(); - requestHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); - requestHeaders.set(HttpHeaders.COOKIE, "value"); - requestHeaders.set(HttpHeaders.AUTHORIZATION, "secret"); - HttpExchangeTracer tracer = new HttpExchangeTracer(Include.defaultIncludes()); - HttpTrace trace = tracer.receivedRequest(createRequest(requestHeaders)); - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.set(HttpHeaders.SET_COOKIE, "test=test"); - responseHeaders.setContentLength(0); - tracer.sendingResponse(trace, createResponse(responseHeaders), this::createPrincipal, () -> "sessionId"); - assertThat(trace.getTimeTaken()).isNotNull(); - assertThat(trace.getPrincipal()).isNull(); - assertThat(trace.getSession()).isNull(); - assertThat(trace.getTimestamp()).isNotNull(); - assertThat(trace.getRequest().getMethod()).isEqualTo("GET"); - assertThat(trace.getRequest().getRemoteAddress()).isNull(); - assertThat(trace.getResponse().getStatus()).isEqualTo(204); - assertThat(trace.getRequest().getHeaders()).containsOnlyKeys(HttpHeaders.ACCEPT); - assertThat(trace.getResponse().getHeaders()).containsOnlyKeys(HttpHeaders.CONTENT_LENGTH); - } - - 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.toUpperCase(input.charAt(i)) : Character.toLowerCase(input.charAt(i))); - } - return output.toString(); - } - - static class RequestHeadersFilterHttpExchangeTracer extends HttpExchangeTracer { - - RequestHeadersFilterHttpExchangeTracer() { - super(EnumSet.of(Include.REQUEST_HEADERS)); - } - - @Override - protected void postProcessRequestHeaders(Map> headers) { - headers.remove("to-remove"); - headers.computeIfAbsent("to-add", (key) -> Collections.singletonList("42")); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/HttpTraceEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/HttpTraceEndpointTests.java deleted file mode 100644 index dc347b8ea4..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/HttpTraceEndpointTests.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.trace.http; - -import java.util.List; - -import org.junit.jupiter.api.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 - */ -class HttpTraceEndpointTests { - - @Test - 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/exchanges/HttpExchangeTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/HttpExchangeTests.java new file mode 100644 index 0000000000..6048821350 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/HttpExchangeTests.java @@ -0,0 +1,335 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.web.exchanges; + +import java.net.URI; +import java.security.Principal; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test for {@link HttpExchange}. + * + * @author Andy Wilkinson + * @author Phillip Webb + */ +class HttpExchangeTests { + + private static final Map> AUTHORIZATION_HEADER = Map.of(HttpHeaders.AUTHORIZATION, + Arrays.asList("secret")); + + private static final Map> COOKIE_HEADER = Map.of(HttpHeaders.COOKIE, + Arrays.asList("test=test")); + + private static final Map> SET_COOKIE_HEADER = Map.of(HttpHeaders.SET_COOKIE, + Arrays.asList("test=test")); + + private static final Supplier NO_PRINCIPAL = () -> null; + + private static final Supplier NO_SESSION_ID = () -> null; + + private static final Supplier WITH_PRINCIPAL = () -> { + Principal principal = mock(Principal.class); + given(principal.getName()).willReturn("alice"); + return principal; + }; + + private static final Supplier WITH_SESSION_ID = () -> "JSESSION_123"; + + @Test + void getTimestampReturnsTimestamp() { + Instant now = Instant.now(); + Clock clock = Clock.fixed(now, ZoneId.systemDefault()); + HttpExchange exchange = HttpExchange.start(clock, createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getTimestamp()).isEqualTo(now); + } + + @Test + void getRequestUriReturnsUri() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getRequest().getUri()).isEqualTo(URI.create("https://api.example.com")); + } + + @Test + void getRequestRemoteAddressWhenUsingDefaultIncludesReturnsNull() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getRequest().getRemoteAddress()).isNull(); + } + + @Test + void getRequestRemoteAddressWhenIncludedReturnsRemoteAddress() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.REMOTE_ADDRESS); + assertThat(exchange.getRequest().getRemoteAddress()).isEqualTo("127.0.0.1"); + } + + @Test + void getRequestMethodReturnsHttpMethod() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getRequest().getMethod()).isEqualTo("GET"); + } + + @Test + void getRequestHeadersWhenUsingDefaultIncludesReturnsHeaders() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getRequest().getHeaders()).containsOnlyKeys(HttpHeaders.ACCEPT); + } + + @Test + void getRequestHeadersWhenIncludedReturnsHeaders() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.REQUEST_HEADERS); + assertThat(exchange.getRequest().getHeaders()).containsOnlyKeys(HttpHeaders.ACCEPT); + } + + @Test + void getRequestHeadersWhenNotIncludedReturnsEmptyHeaders() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID); + assertThat(exchange.getRequest().getHeaders()).isEmpty(); + } + + @Test + void getRequestHeadersWhenUsingDefaultIncludesFiltersAuthorizeHeader() { + HttpExchange exchange = HttpExchange.start(createRequest(AUTHORIZATION_HEADER)).finish(createResponse(), + NO_PRINCIPAL, NO_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getRequest().getHeaders()).isEmpty(); + } + + @Test + void getRequestHeadersWhenIncludesAuthorizationHeaderReturnsHeaders() { + HttpExchange exchange = HttpExchange.start(createRequest(AUTHORIZATION_HEADER)).finish(createResponse(), + NO_PRINCIPAL, NO_SESSION_ID, Include.REQUEST_HEADERS, Include.AUTHORIZATION_HEADER); + assertThat(exchange.getRequest().getHeaders()).containsOnlyKeys(HttpHeaders.AUTHORIZATION); + } + + @Test + void getRequestHeadersWhenIncludesAuthorizationHeaderAndInDifferentCaseReturnsHeaders() { + HttpExchange exchange = HttpExchange.start(createRequest(mixedCase(AUTHORIZATION_HEADER))).finish( + createResponse(), NO_PRINCIPAL, NO_SESSION_ID, Include.REQUEST_HEADERS, Include.AUTHORIZATION_HEADER); + assertThat(exchange.getRequest().getHeaders()).containsOnlyKeys(mixedCase(HttpHeaders.AUTHORIZATION)); + } + + @Test + void getRequestHeadersWhenUsingDefaultIncludesFiltersCookieHeader() { + HttpExchange exchange = HttpExchange.start(createRequest(COOKIE_HEADER)).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getRequest().getHeaders()).isEmpty(); + } + + @Test + void getRequestHeadersWhenIncludesCookieHeaderReturnsHeaders() { + HttpExchange exchange = HttpExchange.start(createRequest(COOKIE_HEADER)).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.REQUEST_HEADERS, Include.COOKIE_HEADERS); + assertThat(exchange.getRequest().getHeaders()).containsOnlyKeys(HttpHeaders.COOKIE); + } + + @Test + void getRequestHeadersWhenIncludesCookieHeaderAndInDifferentCaseReturnsHeaders() { + HttpExchange exchange = HttpExchange.start(createRequest(mixedCase(COOKIE_HEADER))).finish(createResponse(), + NO_PRINCIPAL, NO_SESSION_ID, Include.REQUEST_HEADERS, Include.COOKIE_HEADERS); + assertThat(exchange.getRequest().getHeaders()).containsOnlyKeys(mixedCase(HttpHeaders.COOKIE)); + } + + @Test + void getResponseStatusReturnsStatus() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.REMOTE_ADDRESS); + assertThat(exchange.getResponse().getStatus()).isEqualTo(204); + } + + @Test + void getResponseHeadersWhenUsingDefaultIncludesReturnsHeaders() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getResponse().getHeaders()).containsOnlyKeys(HttpHeaders.CONTENT_TYPE); + } + + @Test + void getResponseHeadersWhenNotIncludedReturnsEmptyHeaders() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID); + assertThat(exchange.getResponse().getHeaders()).isEmpty(); + } + + @Test + void getResponseHeadersIncludedReturnsHeaders() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.RESPONSE_HEADERS); + assertThat(exchange.getResponse().getHeaders()).containsOnlyKeys(HttpHeaders.CONTENT_TYPE); + } + + @Test + void getResponseHeadersWhenUsingDefaultIncludesFiltersSetCookieHeader() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(SET_COOKIE_HEADER), + NO_PRINCIPAL, NO_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getResponse().getHeaders()).isEmpty(); + } + + @Test + void getResponseHeadersWhenIncludesCookieHeaderReturnsHeaders() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(SET_COOKIE_HEADER), + NO_PRINCIPAL, NO_SESSION_ID, Include.RESPONSE_HEADERS, Include.COOKIE_HEADERS); + assertThat(exchange.getResponse().getHeaders()).containsKey(HttpHeaders.SET_COOKIE); + } + + @Test + void getResponseHeadersWhenIncludesCookieHeaderAndInDifferentCaseReturnsHeaders() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(mixedCase(SET_COOKIE_HEADER)), + NO_PRINCIPAL, NO_SESSION_ID, Include.RESPONSE_HEADERS, Include.COOKIE_HEADERS); + assertThat(exchange.getResponse().getHeaders()).containsKey(mixedCase(HttpHeaders.SET_COOKIE)); + } + + @Test + void getPrincipalWhenUsingDefaultIncludesReturnsNull() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), WITH_PRINCIPAL, + NO_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getPrincipal()).isNull(); + } + + @Test + void getPrincipalWhenIncludesPrincipalReturnsPrincipal() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), WITH_PRINCIPAL, + NO_SESSION_ID, Include.PRINCIPAL); + assertThat(exchange.getPrincipal()).isNotNull(); + assertThat(exchange.getPrincipal().getName()).isEqualTo("alice"); + } + + @Test + void getSessionIdWhenUsingDefaultIncludesReturnsNull() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + WITH_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getSession()).isNull(); + } + + @Test + void getSessionIdWhenIncludesSessionReturnsSessionId() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + WITH_SESSION_ID, Include.SESSION_ID); + assertThat(exchange.getSession()).isNotNull(); + assertThat(exchange.getSession().getId()).isEqualTo("JSESSION_123"); + } + + @Test + void getTimeTakenWhenUsingDefaultIncludesReturnsTimeTaken() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getTimeTaken()).isNotNull(); + } + + @Test + void getTimeTakenWhenNotIncludedReturnsNull() { + HttpExchange exchange = HttpExchange.start(createRequest()).finish(createResponse(), NO_PRINCIPAL, + NO_SESSION_ID); + assertThat(exchange.getTimeTaken()).isNull(); + } + + @Test + void getTimeTakenWhenIncludesTimeTakenReturnsTimeTaken() { + Duration duration = Duration.ofSeconds(1); + Clock startClock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + Clock finishClock = Clock.offset(startClock, duration); + HttpExchange exchange = HttpExchange.start(startClock, createRequest()).finish(finishClock, createResponse(), + NO_PRINCIPAL, NO_SESSION_ID, Include.TIME_TAKEN); + assertThat(exchange.getTimeTaken()).isEqualTo(duration); + } + + @Test + void defaultIncludes() { + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + requestHeaders.set(HttpHeaders.COOKIE, "value"); + requestHeaders.set(HttpHeaders.AUTHORIZATION, "secret"); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.set(HttpHeaders.SET_COOKIE, "test=test"); + responseHeaders.setContentLength(0); + HttpExchange exchange = HttpExchange.start(createRequest(requestHeaders)) + .finish(createResponse(responseHeaders), NO_PRINCIPAL, NO_SESSION_ID, Include.defaultIncludes()); + assertThat(exchange.getTimeTaken()).isNotNull(); + assertThat(exchange.getPrincipal()).isNull(); + assertThat(exchange.getSession()).isNull(); + assertThat(exchange.getTimestamp()).isNotNull(); + assertThat(exchange.getRequest().getMethod()).isEqualTo("GET"); + assertThat(exchange.getRequest().getRemoteAddress()).isNull(); + assertThat(exchange.getResponse().getStatus()).isEqualTo(204); + assertThat(exchange.getRequest().getHeaders()).containsOnlyKeys(HttpHeaders.ACCEPT); + assertThat(exchange.getResponse().getHeaders()).containsOnlyKeys(HttpHeaders.CONTENT_LENGTH); + } + + private SourceHttpRequest createRequest() { + return createRequest(Collections.singletonMap(HttpHeaders.ACCEPT, Arrays.asList("application/json"))); + } + + private SourceHttpRequest createRequest(Map> headers) { + SourceHttpRequest request = mock(SourceHttpRequest.class); + given(request.getMethod()).willReturn("GET"); + given(request.getUri()).willReturn(URI.create("https://api.example.com")); + given(request.getHeaders()).willReturn(new HashMap<>(headers)); + given(request.getRemoteAddress()).willReturn("127.0.0.1"); + return request; + } + + private SourceHttpResponse createResponse() { + return createResponse(Collections.singletonMap(HttpHeaders.CONTENT_TYPE, Arrays.asList("application/json"))); + } + + private SourceHttpResponse createResponse(Map> headers) { + SourceHttpResponse response = mock(SourceHttpResponse.class); + given(response.getStatus()).willReturn(204); + given(response.getHeaders()).willReturn(new HashMap<>(headers)); + return response; + } + + private Map> mixedCase(Map> headers) { + Map> result = new LinkedHashMap<>(); + headers.forEach((key, value) -> result.put(mixedCase(key), value)); + return result; + } + + private String mixedCase(String input) { + StringBuilder output = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + char ch = input.charAt(i); + output.append((i % 2 != 0) ? Character.toUpperCase(ch) : Character.toLowerCase(ch)); + } + return output.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/HttpExchangesEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/HttpExchangesEndpointTests.java new file mode 100644 index 0000000000..76e5773c53 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/HttpExchangesEndpointTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.web.exchanges; + +import java.security.Principal; +import java.util.List; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HttpExchangesEndpoint}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class HttpExchangesEndpointTests { + + private static final Supplier NO_PRINCIPAL = () -> null; + + private static final Supplier NO_SESSION_ID = () -> null; + + @Test + void httpExchanges() { + HttpExchangeRepository repository = new InMemoryHttpExchangeRepository(); + repository.add(HttpExchange.start(createRequest("GET")).finish(createResponse(), NO_PRINCIPAL, NO_SESSION_ID)); + List httpExchanges = new HttpExchangesEndpoint(repository).httpExchanges().getExchanges(); + assertThat(httpExchanges).hasSize(1); + HttpExchange trace = httpExchanges.get(0); + assertThat(trace.getRequest().getMethod()).isEqualTo("GET"); + } + + private SourceHttpRequest createRequest(String method) { + SourceHttpRequest request = mock(SourceHttpRequest.class); + given(request.getMethod()).willReturn(method); + return request; + } + + private SourceHttpResponse createResponse() { + return mock(SourceHttpResponse.class); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/InMemoryHttpTraceRepositoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/InMemoryHttpExchangeRepositoryTests.java similarity index 51% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/InMemoryHttpTraceRepositoryTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/InMemoryHttpExchangeRepositoryTests.java index e9e3df49a6..c78d070869 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/InMemoryHttpTraceRepositoryTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/InMemoryHttpExchangeRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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,9 +14,11 @@ * limitations under the License. */ -package org.springframework.boot.actuate.trace.http; +package org.springframework.boot.actuate.web.exchanges; +import java.security.Principal; import java.util.List; +import java.util.function.Supplier; import org.junit.jupiter.api.Test; @@ -25,44 +27,49 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Tests for {@link InMemoryHttpTraceRepository}. + * Tests for {@link InMemoryHttpExchangeRepository}. * * @author Dave Syer * @author Andy Wilkinson */ -class InMemoryHttpTraceRepositoryTests { +class InMemoryHttpExchangeRepositoryTests { - private final InMemoryHttpTraceRepository repository = new InMemoryHttpTraceRepository(); + private static final Supplier NO_PRINCIPAL = () -> null; + + private static final Supplier NO_SESSION_ID = () -> null; + + private final InMemoryHttpExchangeRepository repository = new InMemoryHttpExchangeRepository(); @Test - void capacityLimited() { + void adWhenHasLimitedCapacityRestrictsSize() { 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(); + this.repository.add(createHttpExchange("GET")); + this.repository.add(createHttpExchange("POST")); + this.repository.add(createHttpExchange("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 - void reverseFalse() { + void addWhenReverseFalseReturnsInCorrectOrder() { 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(); + this.repository.add(createHttpExchange("GET")); + this.repository.add(createHttpExchange("POST")); + this.repository.add(createHttpExchange("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); + private HttpExchange createHttpExchange(String method) { + SourceHttpRequest request = mock(SourceHttpRequest.class); given(request.getMethod()).willReturn(method); - return request; + SourceHttpResponse response = mock(SourceHttpResponse.class); + return HttpExchange.start(request).finish(response, NO_PRINCIPAL, NO_SESSION_ID); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/HttpExchangesWebFilterIntegrationTests.java similarity index 75% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterIntegrationTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/HttpExchangesWebFilterIntegrationTests.java index a46547c046..4261d7d05b 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/HttpExchangesWebFilterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2022 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,19 +14,16 @@ * limitations under the License. */ -package org.springframework.boot.actuate.trace.http.reactive; +package org.springframework.boot.actuate.web.exchanges.reactive; import java.util.EnumSet; -import java.util.Set; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; -import org.springframework.boot.actuate.trace.http.HttpExchangeTracer; -import org.springframework.boot.actuate.trace.http.HttpTraceRepository; -import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository; -import org.springframework.boot.actuate.trace.http.Include; -import org.springframework.boot.actuate.web.trace.reactive.HttpTraceWebFilter; +import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository; +import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository; +import org.springframework.boot.actuate.web.exchanges.Include; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -45,11 +42,11 @@ import static org.springframework.web.reactive.function.server.RequestPredicates import static org.springframework.web.reactive.function.server.RouterFunctions.route; /** - * Integration tests for {@link HttpTraceWebFilter}. + * Integration tests for {@link HttpExchangesWebFilter}. * * @author Andy Wilkinson */ -class HttpTraceWebFilterIntegrationTests { +class HttpExchangesWebFilterIntegrationTests { private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withUserConfiguration(Config.class); @@ -59,7 +56,7 @@ class HttpTraceWebFilterIntegrationTests { this.contextRunner.run((context) -> { WebTestClient.bindToApplicationContext(context).build().get().uri("/").exchange().expectStatus() .isNotFound(); - HttpTraceRepository repository = context.getBean(HttpTraceRepository.class); + HttpExchangeRepository repository = context.getBean(HttpExchangeRepository.class); assertThat(repository.findAll()).hasSize(1); assertThat(repository.findAll().get(0).getResponse().getStatus()).isEqualTo(404); }); @@ -70,7 +67,7 @@ class HttpTraceWebFilterIntegrationTests { this.contextRunner.run((context) -> { WebTestClient.bindToApplicationContext(context).build().get().uri("/mono-error").exchange().expectStatus() .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - HttpTraceRepository repository = context.getBean(HttpTraceRepository.class); + HttpExchangeRepository repository = context.getBean(HttpExchangeRepository.class); assertThat(repository.findAll()).hasSize(1); assertThat(repository.findAll().get(0).getResponse().getStatus()).isEqualTo(500); }); @@ -81,7 +78,7 @@ class HttpTraceWebFilterIntegrationTests { this.contextRunner.run((context) -> { WebTestClient.bindToApplicationContext(context).build().get().uri("/thrown").exchange().expectStatus() .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - HttpTraceRepository repository = context.getBean(HttpTraceRepository.class); + HttpExchangeRepository repository = context.getBean(HttpExchangeRepository.class); assertThat(repository.findAll()).hasSize(1); assertThat(repository.findAll().get(0).getResponse().getStatus()).isEqualTo(500); }); @@ -92,14 +89,13 @@ class HttpTraceWebFilterIntegrationTests { static class Config { @Bean - HttpTraceWebFilter httpTraceWebFilter(HttpTraceRepository repository) { - Set includes = EnumSet.allOf(Include.class); - return new HttpTraceWebFilter(repository, new HttpExchangeTracer(includes), includes); + HttpExchangesWebFilter httpExchangesWebFilter(HttpExchangeRepository repository) { + return new HttpExchangesWebFilter(repository, EnumSet.allOf(Include.class)); } @Bean - HttpTraceRepository httpTraceRepository() { - return new InMemoryHttpTraceRepository(); + HttpExchangeRepository httpExchangeRepository() { + return new InMemoryHttpExchangeRepository(); } @Bean diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/HttpExchangesWebFilterTests.java similarity index 77% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/HttpExchangesWebFilterTests.java index afecd178f8..db3bb34660 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/HttpExchangesWebFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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.http.reactive; +package org.springframework.boot.actuate.web.exchanges.reactive; import java.security.Principal; import java.util.EnumSet; @@ -23,11 +23,9 @@ import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import org.springframework.boot.actuate.trace.http.HttpExchangeTracer; -import org.springframework.boot.actuate.trace.http.HttpTrace.Session; -import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository; -import org.springframework.boot.actuate.trace.http.Include; -import org.springframework.boot.actuate.web.trace.reactive.HttpTraceWebFilter; +import org.springframework.boot.actuate.web.exchanges.HttpExchange.Session; +import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository; +import org.springframework.boot.actuate.web.exchanges.Include; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.web.server.ServerWebExchange; @@ -39,17 +37,15 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Tests for {@link HttpTraceWebFilter}. + * Tests for {@link HttpExchangesWebFilter}. * * @author Andy Wilkinson */ -class HttpTraceWebFilterTests { +class HttpExchangesWebFilterTests { - private final InMemoryHttpTraceRepository repository = new InMemoryHttpTraceRepository(); + private final InMemoryHttpExchangeRepository repository = new InMemoryHttpExchangeRepository(); - private final HttpExchangeTracer tracer = new HttpExchangeTracer(EnumSet.allOf(Include.class)); - - private final HttpTraceWebFilter filter = new HttpTraceWebFilter(this.repository, this.tracer, + private final HttpExchangesWebFilter filter = new HttpExchangesWebFilter(this.repository, EnumSet.allOf(Include.class)); @Test @@ -94,8 +90,8 @@ class HttpTraceWebFilterTests { }, (exchange) -> exchange.getSession().doOnNext((session) -> session.getAttributes().put("a", "alpha")).then()); assertThat(this.repository.findAll()).hasSize(1); - org.springframework.boot.actuate.trace.http.HttpTrace.Principal tracedPrincipal = this.repository.findAll() - .get(0).getPrincipal(); + org.springframework.boot.actuate.web.exchanges.HttpExchange.Principal tracedPrincipal = this.repository + .findAll().get(0).getPrincipal(); assertThat(tracedPrincipal).isNotNull(); assertThat(tracedPrincipal.getName()).isEqualTo("alice"); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequestTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/SourceServerHttpRequestTests.java similarity index 77% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequestTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/SourceServerHttpRequestTests.java index 7125741c85..7dc907b6c4 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequestTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/SourceServerHttpRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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.web.trace.reactive; +package org.springframework.boot.actuate.web.exchanges.reactive; import java.net.InetSocketAddress; import java.net.URI; @@ -34,11 +34,11 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Tests for {@link ServerWebExchangeTraceableRequest}. + * Tests for {@link SourceServerHttpRequest}. * * @author Dmytro Nosan */ -class ServerWebExchangeTraceableRequestTests { +class SourceServerHttpRequestTests { private ServerWebExchange exchange; @@ -54,7 +54,7 @@ class ServerWebExchangeTraceableRequestTests { @Test void getMethod() { - ServerWebExchangeTraceableRequest traceableRequest = new ServerWebExchangeTraceableRequest(this.exchange); + SourceServerHttpRequest traceableRequest = new SourceServerHttpRequest(this.request); assertThat(traceableRequest.getMethod()).isEqualTo("GET"); } @@ -62,7 +62,7 @@ class ServerWebExchangeTraceableRequestTests { void getUri() { URI uri = URI.create("http://localhost:8080/"); given(this.request.getURI()).willReturn(uri); - ServerWebExchangeTraceableRequest traceableRequest = new ServerWebExchangeTraceableRequest(this.exchange); + SourceServerHttpRequest traceableRequest = new SourceServerHttpRequest(this.request); assertThat(traceableRequest.getUri()).isSameAs(uri); } @@ -71,7 +71,7 @@ class ServerWebExchangeTraceableRequestTests { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("name", "value"); given(this.request.getHeaders()).willReturn(httpHeaders); - ServerWebExchangeTraceableRequest traceableRequest = new ServerWebExchangeTraceableRequest(this.exchange); + SourceServerHttpRequest traceableRequest = new SourceServerHttpRequest(this.request); assertThat(traceableRequest.getHeaders()).containsOnly(entry("name", Collections.singletonList("value"))); } @@ -79,7 +79,7 @@ class ServerWebExchangeTraceableRequestTests { void getUnresolvedRemoteAddress() { InetSocketAddress socketAddress = InetSocketAddress.createUnresolved("unresolved.example.com", 8080); given(this.request.getRemoteAddress()).willReturn(socketAddress); - ServerWebExchangeTraceableRequest traceableRequest = new ServerWebExchangeTraceableRequest(this.exchange); + SourceServerHttpRequest traceableRequest = new SourceServerHttpRequest(this.request); assertThat(traceableRequest.getRemoteAddress()).isNull(); } @@ -87,7 +87,7 @@ class ServerWebExchangeTraceableRequestTests { void getRemoteAddress() { InetSocketAddress socketAddress = new InetSocketAddress(0); given(this.request.getRemoteAddress()).willReturn(socketAddress); - ServerWebExchangeTraceableRequest traceableRequest = new ServerWebExchangeTraceableRequest(this.exchange); + SourceServerHttpRequest traceableRequest = new SourceServerHttpRequest(this.request); assertThat(traceableRequest.getRemoteAddress()).isEqualTo(socketAddress.getAddress().toString()); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/servlet/HttpTraceFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/servlet/HttpExchangesFilterTests.java similarity index 80% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/servlet/HttpTraceFilterTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/servlet/HttpExchangesFilterTests.java index 5d0abc608a..7a82abf38b 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/servlet/HttpTraceFilterTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/servlet/HttpExchangesFilterTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.actuate.trace.http.servlet; +package org.springframework.boot.actuate.web.exchanges.servlet; import java.io.IOException; import java.security.Principal; @@ -26,11 +26,9 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.trace.http.HttpExchangeTracer; -import org.springframework.boot.actuate.trace.http.HttpTrace.Session; -import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository; -import org.springframework.boot.actuate.trace.http.Include; -import org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter; +import org.springframework.boot.actuate.web.exchanges.HttpExchange.Session; +import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository; +import org.springframework.boot.actuate.web.exchanges.Include; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -41,7 +39,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Tests for {@link HttpTraceFilter}. + * Tests for {@link HttpExchangesFilter}. * * @author Dave Syer * @author Wallace Wadge @@ -51,13 +49,11 @@ import static org.mockito.Mockito.mock; * @author Stephane Nicoll * @author Madhura Bhave */ -class HttpTraceFilterTests { +class HttpExchangesFilterTests { - private final InMemoryHttpTraceRepository repository = new InMemoryHttpTraceRepository(); + private final InMemoryHttpExchangeRepository repository = new InMemoryHttpExchangeRepository(); - private final HttpExchangeTracer tracer = new HttpExchangeTracer(EnumSet.allOf(Include.class)); - - private final HttpTraceFilter filter = new HttpTraceFilter(this.repository, this.tracer); + private final HttpExchangesFilter filter = new HttpExchangesFilter(this.repository, EnumSet.allOf(Include.class)); @Test void filterTracesExchange() throws ServletException, IOException { @@ -91,8 +87,8 @@ class HttpTraceFilterTests { request.setUserPrincipal(principal); this.filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()); assertThat(this.repository.findAll()).hasSize(1); - org.springframework.boot.actuate.trace.http.HttpTrace.Principal tracedPrincipal = this.repository.findAll() - .get(0).getPrincipal(); + org.springframework.boot.actuate.web.exchanges.HttpExchange.Principal tracedPrincipal = this.repository + .findAll().get(0).getPrincipal(); assertThat(tracedPrincipal).isNotNull(); assertThat(tracedPrincipal.getName()).isEqualTo("alice"); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletRequestTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/servlet/ServletSourceHttpRequestTests.java similarity index 84% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletRequestTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/servlet/ServletSourceHttpRequestTests.java index ec68db114c..ed19e04c41 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletRequestTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/servlet/ServletSourceHttpRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2022 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.web.trace.servlet; +package org.springframework.boot.actuate.web.exchanges.servlet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -24,11 +24,11 @@ import org.springframework.mock.web.MockHttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link TraceableHttpServletRequest}. + * Tests for {@link ServletSourceHttpRequest}. * * @author Madhura Bhave */ -class TraceableHttpServletRequestTests { +class ServletSourceHttpRequestTests { private MockHttpServletRequest request; @@ -61,7 +61,7 @@ class TraceableHttpServletRequestTests { } private void validate(String expectedUri) { - TraceableHttpServletRequest trace = new TraceableHttpServletRequest(this.request); + ServletSourceHttpRequest trace = new ServletSourceHttpRequest(this.request); assertThat(trace.getUri().toString()).isEqualTo(expectedUri); } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator.adoc index d3efcf5708..d32bb57246 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator.adoc @@ -27,7 +27,7 @@ include::actuator/tracing.adoc[] include::actuator/auditing.adoc[] -include::actuator/http-tracing.adoc[] +include::actuator/http-exchanges.adoc[] include::actuator/process-monitoring.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc index 906a41c048..05a399f38f 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc @@ -44,9 +44,9 @@ The following technology-agnostic endpoints are available: | `health` | Shows application health information. -| `httptrace` -| Displays HTTP trace information (by default, the last 100 HTTP request-response exchanges). - Requires an `HttpTraceRepository` bean. +| `httpexchanges` +| Displays HTTP exchange information (by default, the last 100 HTTP request-response exchanges). + Requires an `HttpExchangeRepository` bean. | `info` | Displays arbitrary application info. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/http-exchanges.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/http-exchanges.adoc new file mode 100644 index 0000000000..a9a8969aca --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/http-exchanges.adoc @@ -0,0 +1,17 @@ +[[actuator.http-exchanges]] +== Recording HTTP Exchanges +You can enable recording of HTTP exchanges by providing a bean of type `HttpExchangeRepository` in your application's configuration. +For convenience, Spring Boot offers `InMemoryHttpExchangeRepository`, which, by default, stores the last 100 request-response exchanges. +`InMemoryHttpExchangeRepository` is limited compared to tracing solutions, and we recommend using it only for development environments. +For production environments, we recommend using a production-ready tracing or observability solution, such as Zipkin or OpenTelemetry. +Alternatively, you can create your own `HttpExchangeRepository`. + +You can use the `httpexchanges` endpoint to obtain information about the request-response exchanges that are stored in the `HttpExchangeRepository`. + + + +[[actuator.http-exchanges.custom]] +=== Custom HTTP Exchange Recording +To customize the items that are included in each recorded exchange, use the configprop:management.httpexchanges.include[] configuration property. + +To disable recoding entirely, set configprop:management.httpexchanges.record[] to `false`. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/http-tracing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/http-tracing.adoc deleted file mode 100644 index 8282dd2e49..0000000000 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/http-tracing.adoc +++ /dev/null @@ -1,16 +0,0 @@ -[[actuator.tracing]] -== HTTP Tracing -You can enable HTTP Tracing by providing a bean of type `HttpTraceRepository` in your application's configuration. -For convenience, Spring Boot offers `InMemoryHttpTraceRepository`, which stores traces for the last 100 (the default) request-response exchanges. -`InMemoryHttpTraceRepository` is limited compared to other tracing solutions, and we recommend using it only for development environments. -For production environments, we recommend using a production-ready tracing or observability solution, such as Zipkin or Spring Cloud Sleuth. -Alternatively, you can create your own `HttpTraceRepository`. - -You can use the `httptrace` endpoint to obtain information about the request-response exchanges that are stored in the `HttpTraceRepository`. - - - -[[actuator.tracing.custom]] -=== Custom HTTP Tracing -To customize the items that are included in each trace, use the configprop:management.trace.http.include[] configuration property. -For advanced customization, consider registering your own `HttpExchangeTracer` implementation. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties index f92f3bd8a6..44916e5ae4 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties @@ -1011,3 +1011,5 @@ data.nosql.elasticsearch.connecting-using-rest.webclient=data.nosql.elasticsearc # Spring Boot 2.7 - 3.0 migrations getting-started.first-application.code.enable-auto-configuration=getting-started.first-application.code.spring-boot-application +actuator.tracing=actuator.http-exchanges +actuator.tracing.custom=actuator.http-exchanges.custom diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc index fec42b9096..3d77c1227b 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc @@ -210,7 +210,7 @@ When it does so, the orders shown in the following table will be used: | `WebFilterChainProxy` (Spring Security) | `-100` -| `HttpTraceWebFilter` +| `HttpExchangesWebFilter` | `Ordered.LOWEST_PRECEDENCE - 10` |=== diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties index 9b4f59ab8a..ead47936e9 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties @@ -17,7 +17,7 @@ spring.jmx.enabled=true spring.jackson.serialization.write_dates_as_timestamps=false -management.trace.http.include=request-headers,response-headers,principal,remote-address,session-id +management.httpexchanges.include=request-headers,response-headers,principal,remote-address,session-id management.endpoint.health.show-details=always management.endpoint.health.group.ready.include=db,diskSpace