Rename httptrace endpoint and related classes to httpexchanges

Rename `/actuator/httptrace` to `/actuator/httpexchanges` to better
describe its purpose and to remove confusion with distribute tracing.

This change also takes the opportunity to improve the code by making
the `HttpExchange` class (previously `HttpTrace`) fully immutable.

Closes gh-32885

Co-authored-by: Andy Wilkinson <wilkinsona@vmware.com>
pull/32920/head
Phillip Webb 2 years ago
parent 26d61b9295
commit 3e50836b1a

@ -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

@ -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[]

@ -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[]

@ -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]

@ -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());
}
}

@ -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);
}
}

@ -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

@ -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;

@ -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"
}
},

@ -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

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}

@ -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();
}
}

@ -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();
}
}

@ -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<HttpExchange> 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<Include> 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<Include> includes) {
super(repository, includes);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomFilterConfiguration {
@Bean
CustomHttpExchangesFilter customWebFilter(HttpExchangeRepository repository, Set<Include> includes) {
return new CustomHttpExchangesFilter(repository, includes);
}
}
}

@ -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();
}
}

@ -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<HttpTrace> 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<Include> 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<Include> 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);
}
}
}

@ -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<Include> 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<Include> 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> principal,
Supplier<String> 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<String, List<String>> headers) {
}
private <T> T getIfIncluded(Include include, Supplier<T> valueSupplier) {
return this.includes.contains(include) ? valueSupplier.get() : null;
}
private <T> void setIfIncluded(Include include, Supplier<T> supplier, Consumer<T> consumer) {
if (this.includes.contains(include)) {
consumer.accept(supplier.get());
}
}
private Map<String, List<String>> getHeadersIfIncluded(Include include,
Supplier<Map<String, List<String>>> headersSupplier, Predicate<String> 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<String, List<String>> getHeaders() {
Map<String, List<String>> 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<String, List<String>> 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;
}
}
}

@ -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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> headers) {
this.status = status;
this.headers = new LinkedHashMap<>(headers);
}
public int getStatus() {
return this.status;
}
public Map<String, List<String>> 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;
}
}
}

@ -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<java.security.Principal> principalSupplier, Supplier<String> 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<java.security.Principal> principalSupplier, Supplier<String> 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<java.security.Principal> principalSupplier, Supplier<String> sessionIdSupplier,
Set<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<java.security.Principal> principalSupplier, Supplier<String> sessionIdSupplier,
Set<Include> 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> T getIfIncluded(Set<Include> includes, Include include, Supplier<T> 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<String, List<String>> headers;
private Request(SourceHttpRequest source, Set<Include> 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<String, List<String>> headers) {
this.uri = uri;
this.remoteAddress = remoteAddress;
this.method = method;
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers));
}
private Map<String, List<String>> filterHeaders(Map<String, List<String>> headers, Set<Include> 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<String, List<String>> 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<String, List<String>> headers;
private Response(SourceHttpResponse source, Set<Include> 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<String, List<String>> headers) {
this.status = status;
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers));
}
private Map<String, List<String>> filterHeaders(Map<String, List<String>> headers, Set<Include> 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<String, List<String>> 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<String> 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<java.security.Principal> 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<Include> includes;
private final Include requiredInclude;
private final Set<String> filteredHeaderNames;
HeadersFilter(Set<Include> 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<String, List<String>> apply(Map<String, List<String>> headers) {
if (!this.includes.contains(this.requiredInclude)) {
return Collections.emptyMap();
}
Map<String, List<String>> filtered = new LinkedHashMap<>();
headers.forEach((name, value) -> {
if (!this.filteredHeaderNames.contains(name.toLowerCase())) {
filtered.put(name, value);
}
});
return filtered;
}
}
}

@ -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<HttpTrace> findAll();
List<HttpExchange> 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);
}

@ -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<HttpTrace> traces;
private final List<HttpExchange> exchanges;
private HttpTraceDescriptor(List<HttpTrace> traces) {
this.traces = traces;
private HttpExchanges(List<HttpExchange> exchanges) {
this.exchanges = exchanges;
}
public List<HttpTrace> getTraces() {
return this.traces;
public List<HttpExchange> getExchanges() {
return this.exchanges;
}
}

@ -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<HttpTrace> traces = new LinkedList<>();
private final List<HttpExchange> 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<HttpTrace> findAll() {
synchronized (this.traces) {
return Collections.unmodifiableList(new ArrayList<>(this.traces));
public List<HttpExchange> 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);
}
}
}

@ -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<Include> 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);
}

@ -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<String, List<String>> 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<String, List<String>> getHeaders();
}

@ -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.

@ -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;

@ -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<Include> 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<Include> 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<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Mono<?> principal = exchange.getPrincipal().cast(Object.class).defaultIfEmpty(NONE);
Mono<Object> session = exchange.getSession().cast(Object.class).defaultIfEmpty(NONE);
return Mono.zip(PrincipalAndSession::new, principal, session)
.flatMap((principalAndSession) -> filter(exchange, chain, principalAndSession));
}
private Mono<Void> 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;
}
}
}

@ -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();

@ -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<String, List<String>> headers;
TraceableServerHttpResponse(ServerHttpResponse response) {
SourceServerHttpResponse(ServerHttpResponse response) {
this.status = (response.getStatusCode() != null) ? response.getStatusCode().value() : HttpStatus.OK.value();
this.headers = new LinkedHashMap<>(response.getHeaders());
}

@ -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;

@ -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<Include> 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<Include> 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;
}
}
}

@ -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;
}

@ -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<String, List<String>> getHeaders() {
return extractHeaders();
}
private Map<String, List<String>> extractHeaders() {
Map<String, List<String>> headers = new LinkedHashMap<>();
for (String name : this.delegate.getHeaderNames()) {
headers.put(name, new ArrayList<>(this.delegate.getHeaders(name)));

@ -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;

@ -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<Include> includes;
public HttpTraceWebFilter(HttpTraceRepository repository, HttpExchangeTracer tracer, Set<Include> 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<Void> 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> T asType(Object object, Class<T> type) {
if (type.isInstance(object)) {
return type.cast(object);
}
return null;
}
private Mono<Void> 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;
}
}

@ -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<String, String> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> headers) {
headers.remove("to-remove");
headers.computeIfAbsent("to-add", (key) -> Collections.singletonList("42"));
}
}
}

@ -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<HttpTrace> 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;
}
}

@ -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<String, List<String>> AUTHORIZATION_HEADER = Map.of(HttpHeaders.AUTHORIZATION,
Arrays.asList("secret"));
private static final Map<String, List<String>> COOKIE_HEADER = Map.of(HttpHeaders.COOKIE,
Arrays.asList("test=test"));
private static final Map<String, List<String>> SET_COOKIE_HEADER = Map.of(HttpHeaders.SET_COOKIE,
Arrays.asList("test=test"));
private static final Supplier<Principal> NO_PRINCIPAL = () -> null;
private static final Supplier<String> NO_SESSION_ID = () -> null;
private static final Supplier<Principal> WITH_PRINCIPAL = () -> {
Principal principal = mock(Principal.class);
given(principal.getName()).willReturn("alice");
return principal;
};
private static final Supplier<String> 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<String, List<String>> 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<String, List<String>> headers) {
SourceHttpResponse response = mock(SourceHttpResponse.class);
given(response.getStatus()).willReturn(204);
given(response.getHeaders()).willReturn(new HashMap<>(headers));
return response;
}
private Map<String, List<String>> mixedCase(Map<String, List<String>> headers) {
Map<String, List<String>> 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();
}
}

@ -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<Principal> NO_PRINCIPAL = () -> null;
private static final Supplier<String> 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<HttpExchange> 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);
}
}

@ -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<Principal> NO_PRINCIPAL = () -> null;
private static final Supplier<String> 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<HttpTrace> traces = this.repository.findAll();
this.repository.add(createHttpExchange("GET"));
this.repository.add(createHttpExchange("POST"));
this.repository.add(createHttpExchange("DELETE"));
List<HttpExchange> 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<HttpTrace> traces = this.repository.findAll();
this.repository.add(createHttpExchange("GET"));
this.repository.add(createHttpExchange("POST"));
this.repository.add(createHttpExchange("DELETE"));
List<HttpExchange> 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);
}
}

@ -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<Include> 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

@ -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");
}

@ -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());
}

@ -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");
}

@ -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);
}

@ -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[]

@ -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.

@ -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`.

@ -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.

@ -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

@ -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`
|===

@ -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

Loading…
Cancel
Save