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
parent
26d61b9295
commit
3e50836b1a
@ -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[]
|
|
44
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceAutoConfiguration.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesAutoConfiguration.java
44
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceAutoConfiguration.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesAutoConfiguration.java
23
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceEndpointAutoConfiguration.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesEndpointAutoConfiguration.java
23
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceEndpointAutoConfiguration.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesEndpointAutoConfiguration.java
10
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceProperties.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesProperties.java
10
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceProperties.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesProperties.java
@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.exchanges.HttpExchange;
|
||||||
|
import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
|
||||||
|
import org.springframework.boot.actuate.web.exchanges.HttpExchangesEndpoint;
|
||||||
|
import org.springframework.boot.actuate.web.exchanges.Include;
|
||||||
|
import org.springframework.boot.actuate.web.exchanges.SourceHttpRequest;
|
||||||
|
import org.springframework.boot.actuate.web.exchanges.SourceHttpResponse;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.restdocs.payload.JsonFieldType;
|
||||||
|
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||||
|
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||||
|
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for generating documentation describing {@link HttpExchangesEndpoint}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
class HttpExchangesEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private HttpExchangeRepository repository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void httpExchanges() throws Exception {
|
||||||
|
SourceHttpRequest request = mock(SourceHttpRequest.class);
|
||||||
|
given(request.getUri()).willReturn(URI.create("https://api.example.com"));
|
||||||
|
given(request.getMethod()).willReturn("GET");
|
||||||
|
given(request.getHeaders())
|
||||||
|
.willReturn(Collections.singletonMap(HttpHeaders.ACCEPT, Arrays.asList("application/json")));
|
||||||
|
SourceHttpResponse response = mock(SourceHttpResponse.class);
|
||||||
|
given(response.getStatus()).willReturn(200);
|
||||||
|
given(response.getHeaders())
|
||||||
|
.willReturn(Collections.singletonMap(HttpHeaders.CONTENT_TYPE, Arrays.asList("application/json")));
|
||||||
|
Principal principal = mock(Principal.class);
|
||||||
|
given(principal.getName()).willReturn("alice");
|
||||||
|
Instant instant = Instant.parse("2022-12-22T13:43:41.00Z");
|
||||||
|
Clock start = Clock.fixed(instant, ZoneId.systemDefault());
|
||||||
|
Clock end = Clock.offset(start, Duration.ofMillis(23));
|
||||||
|
HttpExchange exchange = HttpExchange.start(start, request).finish(end, response, () -> principal,
|
||||||
|
() -> UUID.randomUUID().toString(), EnumSet.allOf(Include.class));
|
||||||
|
given(this.repository.findAll()).willReturn(Arrays.asList(exchange));
|
||||||
|
this.mockMvc.perform(get("/actuator/httpexchanges")).andExpect(status().isOk()).andDo(document("httpexchanges",
|
||||||
|
responseFields(fieldWithPath("exchanges").description("An array of HTTP request-response exchanges."),
|
||||||
|
fieldWithPath("exchanges.[].timestamp").description("Timestamp of when the exchange occurred."),
|
||||||
|
fieldWithPath("exchanges.[].principal").description("Principal of the exchange, if any.")
|
||||||
|
.optional(),
|
||||||
|
fieldWithPath("exchanges.[].principal.name").description("Name of the principal.").optional(),
|
||||||
|
fieldWithPath("exchanges.[].request.method").description("HTTP method of the request."),
|
||||||
|
fieldWithPath("exchanges.[].request.remoteAddress")
|
||||||
|
.description("Remote address from which the request was received, if known.").optional()
|
||||||
|
.type(JsonFieldType.STRING),
|
||||||
|
fieldWithPath("exchanges.[].request.uri").description("URI of the request."),
|
||||||
|
fieldWithPath("exchanges.[].request.headers")
|
||||||
|
.description("Headers of the request, keyed by header name."),
|
||||||
|
fieldWithPath("exchanges.[].request.headers.*.[]").description("Values of the header"),
|
||||||
|
fieldWithPath("exchanges.[].response.status").description("Status of the response"),
|
||||||
|
fieldWithPath("exchanges.[].response.headers")
|
||||||
|
.description("Headers of the response, keyed by header name."),
|
||||||
|
fieldWithPath("exchanges.[].response.headers.*.[]").description("Values of the header"),
|
||||||
|
fieldWithPath("exchanges.[].session")
|
||||||
|
.description("Session associated with the exchange, if any.").optional(),
|
||||||
|
fieldWithPath("exchanges.[].session.id").description("ID of the session."),
|
||||||
|
fieldWithPath("exchanges.[].timeTaken").description("Time taken to handle the exchange."))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@Import(BaseDocumentationConfiguration.class)
|
||||||
|
static class TestConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
HttpExchangesEndpoint httpExchangesEndpoint(HttpExchangeRepository repository) {
|
||||||
|
return new HttpExchangesEndpoint(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
44
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/HttpTraceEndpointAutoConfigurationTests.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesEndpointAutoConfigurationTests.java
44
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/HttpTraceEndpointAutoConfigurationTests.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesEndpointAutoConfigurationTests.java
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterIntegrationTests.java → spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/HttpExchangesWebFilterIntegrationTests.java
32
spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterIntegrationTests.java → spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/HttpExchangesWebFilterIntegrationTests.java
18
spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequestTests.java → spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/SourceServerHttpRequestTests.java
18
spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequestTests.java → spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/exchanges/reactive/SourceServerHttpRequestTests.java
@ -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.
|
|
Loading…
Reference in New Issue