Commit 3565961d authored by Andy Wilkinson's avatar Andy Wilkinson

Rework HTTP exchange tracing and add support for WebFlux

Closes gh-9980
parent 11064b5d
[[trace]]
= Trace (`trace`)
The `trace` endpoint provides information about HTTP request-response exchanges.
[[trace-retrieving]]
== Retrieving the Traces
To retrieve the traces, make a `GET` request to `/actuator/trace`, as shown in the
following curl-based example:
include::{snippets}trace/curl-request.adoc[]
The resulting response is similar to the following:
include::{snippets}trace/http-response.adoc[]
[[trace-retrieving-response-structure]]
=== Response Structure
The response contains details of the traced HTTP request-response exchanges. The
following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}trace/response-fields.adoc[]
...@@ -68,3 +68,4 @@ include::endpoints/scheduledtasks.adoc[leveloffset=+1] ...@@ -68,3 +68,4 @@ include::endpoints/scheduledtasks.adoc[leveloffset=+1]
include::endpoints/sessions.adoc[leveloffset=+1] include::endpoints/sessions.adoc[leveloffset=+1]
include::endpoints/shutdown.adoc[leveloffset=+1] include::endpoints/shutdown.adoc[leveloffset=+1]
include::endpoints/threaddump.adoc[leveloffset=+1] include::endpoints/threaddump.adoc[leveloffset=+1]
include::endpoints/trace.adoc[leveloffset=+1]
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.trace;
import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link TraceRepository tracing}.
*
* @author Dave Syer
* @since 2.0.0
*/
@Configuration
public class TraceRepositoryAutoConfiguration {
@ConditionalOnMissingBean(TraceRepository.class)
@Bean
public InMemoryTraceRepository traceRepository() {
return new InMemoryTraceRepository();
}
}
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -14,62 +14,69 @@ ...@@ -14,62 +14,69 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.trace; package org.springframework.boot.actuate.autoconfigure.web.trace;
import javax.servlet.Servlet; import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
import javax.servlet.ServletRegistration; import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.web.trace.reactive.HttpTraceWebFilter;
import org.springframework.boot.actuate.trace.TraceRepository; import org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter;
import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for {@link WebRequestTraceFilter * {@link EnableAutoConfiguration Auto-configuration} for HTTP tracing.
* tracing}.
* *
* @author Dave Syer * @author Dave Syer
* @since 2.0.0 * @since 2.0.0
*/ */
@Configuration @Configuration
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, ServletRegistration.class }) @ConditionalOnWebApplication
@AutoConfigureAfter(TraceRepositoryAutoConfiguration.class) @ConditionalOnProperty(prefix = "management.trace", name = "enabled", matchIfMissing = true)
@ConditionalOnProperty(prefix = "management.trace.filter", name = "enabled", matchIfMissing = true) @EnableConfigurationProperties(TraceProperties.class)
@EnableConfigurationProperties(TraceEndpointProperties.class) public class TraceAutoConfiguration {
public class TraceWebFilterAutoConfiguration {
@Bean
@ConditionalOnMissingBean(HttpTraceRepository.class)
public InMemoryHttpTraceRepository traceRepository() {
return new InMemoryHttpTraceRepository();
}
private final TraceRepository traceRepository; @Bean
@ConditionalOnMissingBean
public HttpExchangeTracer httpExchangeTracer(TraceProperties traceProperties) {
return new HttpExchangeTracer(traceProperties.getInclude());
}
private final TraceEndpointProperties endpointProperties; @ConditionalOnWebApplication(type = Type.SERVLET)
static class ServletTraceFilterConfiguration {
private final ErrorAttributes errorAttributes; @Bean
@ConditionalOnMissingBean
public HttpTraceFilter httpTraceFilter(HttpTraceRepository repository,
HttpExchangeTracer tracer) {
return new HttpTraceFilter(repository, tracer);
}
public TraceWebFilterAutoConfiguration(TraceRepository traceRepository,
TraceEndpointProperties endpointProperties,
ObjectProvider<ErrorAttributes> errorAttributes) {
this.traceRepository = traceRepository;
this.endpointProperties = endpointProperties;
this.errorAttributes = errorAttributes.getIfAvailable();
} }
@Bean @ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnMissingBean static class ReactiveTraceFilterConfiguration {
public WebRequestTraceFilter webRequestLoggingFilter() {
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.traceRepository, @Bean
this.endpointProperties.getInclude()); @ConditionalOnMissingBean
if (this.errorAttributes != null) { public HttpTraceWebFilter httpTraceWebFilter(HttpTraceRepository repository,
filter.setErrorAttributes(this.errorAttributes); HttpExchangeTracer tracer, TraceProperties traceProperties) {
return new HttpTraceWebFilter(repository, tracer,
traceProperties.getInclude());
} }
return filter;
} }
} }
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -14,33 +14,34 @@ ...@@ -14,33 +14,34 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.trace; package org.springframework.boot.actuate.autoconfigure.web.trace;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
import org.springframework.boot.actuate.trace.InMemoryTraceRepository; import org.springframework.boot.actuate.web.trace.HttpTraceEndpoint;
import org.springframework.boot.actuate.trace.TraceEndpoint; import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
import org.springframework.boot.actuate.trace.TraceRepository; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for the {@link TraceEndpoint}. * {@link EnableAutoConfiguration Auto-configuration} for the {@link HttpTraceEndpoint}.
* *
* @author Phillip Webb * @author Phillip Webb
* @since 2.0.0 * @since 2.0.0
*/ */
@Configuration @Configuration
@AutoConfigureAfter(TraceAutoConfiguration.class)
public class TraceEndpointAutoConfiguration { public class TraceEndpointAutoConfiguration {
@Bean @Bean
@ConditionalOnBean(HttpTraceRepository.class)
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnEnabledEndpoint @ConditionalOnEnabledEndpoint
public TraceEndpoint traceEndpoint(ObjectProvider<TraceRepository> traceRepository) { public HttpTraceEndpoint traceEndpoint(HttpTraceRepository traceRepository) {
return new TraceEndpoint( return new HttpTraceEndpoint(traceRepository);
traceRepository.getIfAvailable(() -> new InMemoryTraceRepository()));
} }
} }
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -14,12 +14,12 @@ ...@@ -14,12 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.trace; package org.springframework.boot.actuate.autoconfigure.web.trace;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.springframework.boot.actuate.trace.Include; import org.springframework.boot.actuate.web.trace.Include;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
/** /**
...@@ -30,14 +30,15 @@ import org.springframework.boot.context.properties.ConfigurationProperties; ...@@ -30,14 +30,15 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @author Venil Noronha * @author Venil Noronha
* @author Madhura Bhave * @author Madhura Bhave
* @author Stephane Nicoll * @author Stephane Nicoll
* @since 1.3.0 * @since 2.0.0
*/ */
@ConfigurationProperties(prefix = "management.trace") @ConfigurationProperties(prefix = "management.trace")
public class TraceEndpointProperties { public class TraceProperties {
/** /**
* Items to be included in the trace. Defaults to request/response headers (including * Items to be included in the trace. Defaults to request headers (excluding
* cookies) and errors. * Authorization but including Cookie), response headers (including Set-Cookie), and
* time taken.
*/ */
private Set<Include> include = new HashSet<>(Include.defaultIncludes()); private Set<Include> include = new HashSet<>(Include.defaultIncludes());
......
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -17,4 +17,4 @@ ...@@ -17,4 +17,4 @@
/** /**
* Auto-configuration for actuator tracing concerns. * Auto-configuration for actuator tracing concerns.
*/ */
package org.springframework.boot.actuate.autoconfigure.trace; package org.springframework.boot.actuate.autoconfigure.web.trace;
...@@ -185,12 +185,6 @@ ...@@ -185,12 +185,6 @@
"name": "management.info.git.mode", "name": "management.info.git.mode",
"defaultValue": "simple" "defaultValue": "simple"
}, },
{
"name": "management.trace.filter.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable the trace servlet filter.",
"defaultValue": true
},
{ {
"name": "management.metrics.binders.jvm.enabled", "name": "management.metrics.binders.jvm.enabled",
"type": "java.lang.Boolean", "type": "java.lang.Boolean",
...@@ -239,6 +233,21 @@ ...@@ -239,6 +233,21 @@
"description": "Instrument all available connection factories.", "description": "Instrument all available connection factories.",
"defaultValue": true "defaultValue": true
}, },
{
"name": "management.trace.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable HTTP request-response tracing.",
"defaultValue": true
},
{
"name": "management.trace.filter.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable the trace servlet filter.",
"deprecation": {
"replacement": "management.trace.enabled",
"level": "error"
}
},
{ {
"name": "endpoints.actuator.enabled", "name": "endpoints.actuator.enabled",
"type": "java.lang.Boolean", "type": "java.lang.Boolean",
......
...@@ -39,13 +39,12 @@ org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpoint ...@@ -39,13 +39,12 @@ org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpoint
org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.solr.SolrHealthIndicatorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.solr.SolrHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.trace.TraceEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.trace.TraceRepositoryAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.trace.TraceWebFilterAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementContextAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementContextAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.web.trace.TraceAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.web.trace.TraceEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration=\ org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration=\
org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration,\ org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration,\
......
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation;
import java.net.URI;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.UUID;
import org.junit.Test;
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
import org.springframework.boot.actuate.web.trace.HttpTrace;
import org.springframework.boot.actuate.web.trace.HttpTraceEndpoint;
import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
import org.springframework.boot.actuate.web.trace.Include;
import org.springframework.boot.actuate.web.trace.TraceableRequest;
import org.springframework.boot.actuate.web.trace.TraceableResponse;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.payload.JsonFieldType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for generating documentation describing {@link HttpTraceEndpoint}.
*
* @author Andy Wilkinson
*/
public class HttpTraceEndpointDocumentationTests
extends MockMvcEndpointDocumentationTests {
@MockBean
private HttpTraceRepository repository;
@Test
public void traces() throws Exception {
TraceableRequest request = mock(TraceableRequest.class);
given(request.getUri()).willReturn(URI.create("https://api.example.com"));
given(request.getMethod()).willReturn("GET");
given(request.getHeaders()).willReturn(Collections
.singletonMap(HttpHeaders.ACCEPT, Arrays.asList("application/json")));
TraceableResponse response = mock(TraceableResponse.class);
given(response.getStatus()).willReturn(200);
given(response.getHeaders()).willReturn(Collections.singletonMap(
HttpHeaders.CONTENT_TYPE, Arrays.asList("application/json")));
Principal principal = mock(Principal.class);
given(principal.getName()).willReturn("alice");
HttpExchangeTracer tracer = new HttpExchangeTracer(EnumSet.allOf(Include.class));
HttpTrace trace = tracer.receivedRequest(request);
tracer.sendingResponse(trace, response, () -> principal,
() -> UUID.randomUUID().toString());
given(this.repository.findAll()).willReturn(Arrays.asList(trace));
this.mockMvc.perform(get("/actuator/trace")).andExpect(status().isOk())
.andDo(document("trace",
responseFields(
fieldWithPath("traces").description(
"An array of traced HTTP request-response exchanges."),
fieldWithPath("traces.[].timestamp").description(
"Timestamp of when the traced exchange occurred."),
fieldWithPath("traces.[].principal")
.description("Principal of the exchange, if any.")
.optional(),
fieldWithPath("traces.[].principal.name")
.description("Name of the principal.").optional(),
fieldWithPath("traces.[].request.method")
.description("HTTP method of the request."),
fieldWithPath("traces.[].request.remoteAddress")
.description(
"Remote address from which the request was received, if known.")
.optional().type(JsonFieldType.STRING),
fieldWithPath("traces.[].request.uri")
.description("URI of the request."),
fieldWithPath("traces.[].request.headers").description(
"Headers of the request, keyed by header name."),
fieldWithPath("traces.[].request.headers.*.[]")
.description("Values of the header"),
fieldWithPath("traces.[].response.status")
.description("Status of the response"),
fieldWithPath("traces.[].response.headers").description(
"Headers of the response, keyed by header name."),
fieldWithPath("traces.[].response.headers.*.[]")
.description("Values of the header"),
fieldWithPath("traces.[].session")
.description(
"Session associated with the exchange, if any.")
.optional(),
fieldWithPath("traces.[].session.id")
.description("ID of the session."),
fieldWithPath("traces.[].timeTaken").description(
"Time, in milliseconds, taken to handle the exchange."))));
}
@Configuration
@Import(BaseDocumentationConfiguration.class)
static class TestConfiguration {
@Bean
public HttpTraceEndpoint httpTraceEndpoint(HttpTraceRepository repository) {
return new HttpTraceEndpoint(repository);
}
}
}
...@@ -28,8 +28,8 @@ import org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAut ...@@ -28,8 +28,8 @@ import org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAut
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.trace.TraceEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.trace.TraceEndpointAutoConfiguration;
/** /**
* A list of all endpoint auto-configuration classes for use in tests. * A list of all endpoint auto-configuration classes for use in tests.
......
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -28,9 +28,10 @@ import org.junit.Test; ...@@ -28,9 +28,10 @@ import org.junit.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.trace.TraceAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -43,9 +44,10 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -43,9 +44,10 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
public class JmxEndpointIntegrationTests { public class JmxEndpointIntegrationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JmxAutoConfiguration.class, .withConfiguration(AutoConfigurations.of(JmxAutoConfiguration.class,
EndpointAutoConfiguration.class, JmxEndpointAutoConfiguration.class)) EndpointAutoConfiguration.class, JmxEndpointAutoConfiguration.class,
TraceAutoConfiguration.class))
.withConfiguration( .withConfiguration(
AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL)); AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL));
......
...@@ -22,6 +22,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfi ...@@ -22,6 +22,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfi
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.trace.TraceAutoConfiguration;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
...@@ -59,7 +60,8 @@ public class WebMvcEndpointExposureIntegrationTests { ...@@ -59,7 +60,8 @@ public class WebMvcEndpointExposureIntegrationTests {
ManagementContextAutoConfiguration.class, ManagementContextAutoConfiguration.class,
ServletManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class,
ManagementContextAutoConfiguration.class, ManagementContextAutoConfiguration.class,
ServletManagementContextAutoConfiguration.class)) ServletManagementContextAutoConfiguration.class,
TraceAutoConfiguration.class))
.withConfiguration( .withConfiguration(
AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL)) AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL))
.withUserConfiguration(CustomMvcEndpoint.class); .withUserConfiguration(CustomMvcEndpoint.class);
......
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.trace;
import org.junit.Test;
import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link TraceRepositoryAutoConfiguration}.
*
* @author Phillip Webb
*/
public class TraceRepositoryAutoConfigurationTests {
@Test
public void configuresInMemoryTraceRepository() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
TraceRepositoryAutoConfiguration.class);
assertThat(context.getBean(InMemoryTraceRepository.class)).isNotNull();
context.close();
}
@Test
public void skipsIfRepositoryExists() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
Config.class, TraceRepositoryAutoConfiguration.class);
assertThat(context.getBeansOfType(InMemoryTraceRepository.class)).isEmpty();
assertThat(context.getBeansOfType(TraceRepository.class)).hasSize(1);
context.close();
}
@Configuration
public static class Config {
@Bean
public TraceRepository traceRepository() {
return mock(TraceRepository.class);
}
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.trace;
import java.util.Map;
import org.junit.After;
import org.junit.Test;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link TraceWebFilterAutoConfiguration}.
*
* @author Phillip Webb
* @author Stephane Nicoll
*/
public class TraceWebFilterAutoConfigurationTests {
private AnnotationConfigApplicationContext context;
@After
public void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void configureFilter() {
load();
assertThat(this.context.getBean(WebRequestTraceFilter.class)).isNotNull();
}
@Test
public void overrideTraceFilter() {
load(CustomTraceFilterConfig.class);
WebRequestTraceFilter filter = this.context.getBean(WebRequestTraceFilter.class);
assertThat(filter).isInstanceOf(TestWebRequestTraceFilter.class);
}
@Test
public void skipsFilterIfPropertyDisabled() {
load("management.trace.filter.enabled:false");
assertThat(this.context.getBeansOfType(WebRequestTraceFilter.class).size())
.isEqualTo(0);
}
private void load(String... environment) {
load(null, environment);
}
private void load(Class<?> config, String... environment) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of(environment).applyTo(context);
if (config != null) {
context.register(config);
}
context.register(PropertyPlaceholderAutoConfiguration.class,
TraceRepositoryAutoConfiguration.class,
TraceWebFilterAutoConfiguration.class);
context.refresh();
this.context = context;
}
@Configuration
static class CustomTraceFilterConfig {
@Bean
public TestWebRequestTraceFilter testWebRequestTraceFilter(
TraceRepository repository, TraceEndpointProperties properties) {
return new TestWebRequestTraceFilter(repository, properties);
}
}
static class TestWebRequestTraceFilter extends WebRequestTraceFilter {
TestWebRequestTraceFilter(TraceRepository repository,
TraceEndpointProperties properties) {
super(repository, properties.getInclude());
}
@Override
protected void postProcessRequestHeaders(Map<String, Object> headers) {
headers.clear();
}
}
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.web.trace;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
import org.springframework.boot.actuate.web.trace.HttpTrace;
import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository;
import org.springframework.boot.actuate.web.trace.Include;
import org.springframework.boot.actuate.web.trace.reactive.HttpTraceWebFilter;
import org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link TraceAutoConfiguration}.
*
* @author Andy Wilkinson
*/
public class TraceAutoConfigurationTests {
@Test
public void configuresRepository() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
.run((context) -> assertThat(context)
.hasSingleBean(InMemoryHttpTraceRepository.class));
}
@Test
public void usesUserProvidedRepository() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
.withUserConfiguration(CustomRepositoryConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(HttpTraceRepository.class);
assertThat(context.getBean(HttpTraceRepository.class))
.isInstanceOf(CustomHttpTraceRepository.class);
});
}
@Test
public void configuresTracer() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
.run((context) -> assertThat(context)
.hasSingleBean(HttpExchangeTracer.class));
}
@Test
public void usesUserProvidedTracer() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
.withUserConfiguration(CustomTracerConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(HttpExchangeTracer.class);
assertThat(context.getBean(HttpExchangeTracer.class))
.isInstanceOf(CustomHttpExchangeTracer.class);
});
}
@Test
public void configuresWebFilter() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
.run((context) -> assertThat(context)
.hasSingleBean(HttpTraceWebFilter.class));
}
@Test
public void usesUserProvidedWebFilter() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
.withUserConfiguration(CustomWebFilterConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(HttpTraceWebFilter.class);
assertThat(context.getBean(HttpTraceWebFilter.class))
.isInstanceOf(CustomHttpTraceWebFilter.class);
});
}
@Test
public void configuresServletFilter() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
.run((context) -> assertThat(context)
.hasSingleBean(HttpTraceFilter.class));
}
@Test
public void usesUserProvidedServletFilter() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
.withUserConfiguration(CustomFilterConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(HttpTraceFilter.class);
assertThat(context.getBean(HttpTraceFilter.class))
.isInstanceOf(CustomHttpTraceFilter.class);
});
}
@Test
public void backsOffWhenDisabled() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
.withPropertyValues("management.trace.enabled=false")
.run((context) -> assertThat(context)
.doesNotHaveBean(InMemoryHttpTraceRepository.class)
.doesNotHaveBean(HttpExchangeTracer.class)
.doesNotHaveBean(HttpTraceFilter.class));
}
private static class CustomHttpTraceRepository implements HttpTraceRepository {
@Override
public List<HttpTrace> findAll() {
return null;
}
@Override
public void add(HttpTrace trace) {
}
}
@Configuration
static class CustomRepositoryConfiguration {
@Bean
public CustomHttpTraceRepository customRepository() {
return new CustomHttpTraceRepository();
}
}
private static final class CustomHttpExchangeTracer extends HttpExchangeTracer {
private CustomHttpExchangeTracer(Set<Include> includes) {
super(includes);
}
}
@Configuration
static class CustomTracerConfiguration {
@Bean
public CustomHttpExchangeTracer customTracer(TraceProperties properties) {
return new CustomHttpExchangeTracer(properties.getInclude());
}
}
private static final class CustomHttpTraceWebFilter extends HttpTraceWebFilter {
private CustomHttpTraceWebFilter(HttpTraceRepository repository,
HttpExchangeTracer tracer, Set<Include> includes) {
super(repository, tracer, includes);
}
}
@Configuration
static class CustomWebFilterConfiguration {
@Bean
public CustomHttpTraceWebFilter customWebFilter(HttpTraceRepository repository,
HttpExchangeTracer tracer, TraceProperties properties) {
return new CustomHttpTraceWebFilter(repository, tracer,
properties.getInclude());
}
}
private static final class CustomHttpTraceFilter extends HttpTraceFilter {
private CustomHttpTraceFilter(HttpTraceRepository repository,
HttpExchangeTracer tracer) {
super(repository, tracer);
}
}
@Configuration
static class CustomFilterConfiguration {
@Bean
public CustomHttpTraceFilter customWebFilter(HttpTraceRepository repository,
HttpExchangeTracer tracer) {
return new CustomHttpTraceFilter(repository, tracer);
}
}
}
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -14,13 +14,13 @@ ...@@ -14,13 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.trace; package org.springframework.boot.actuate.autoconfigure.web.trace;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.actuate.trace.TraceEndpoint; import org.springframework.boot.actuate.web.trace.HttpTraceEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -31,21 +31,28 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -31,21 +31,28 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
public class TraceEndpointAutoConfigurationTests { public class TraceEndpointAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration( .withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class,
AutoConfigurations.of(TraceEndpointAutoConfiguration.class)); TraceEndpointAutoConfiguration.class));
@Test @Test
public void runShouldHaveEndpointBean() { public void runShouldHaveEndpointBean() {
this.contextRunner this.contextRunner.run(
.run((context) -> assertThat(context).hasSingleBean(TraceEndpoint.class)); (context) -> assertThat(context).hasSingleBean(HttpTraceEndpoint.class));
} }
@Test @Test
public void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() { public void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() {
this.contextRunner.withPropertyValues("management.endpoint.trace.enabled:false") this.contextRunner.withPropertyValues("management.endpoint.trace.enabled:false")
.run((context) -> assertThat(context) .run((context) -> assertThat(context)
.doesNotHaveBean(TraceEndpoint.class)); .doesNotHaveBean(HttpTraceEndpoint.class));
}
@Test
public void endpointBacksOffWhenRepositoryIsNotAvailable() {
this.contextRunner.withPropertyValues("management.trace.enabled:false")
.run((context) -> assertThat(context)
.doesNotHaveBean(HttpTraceEndpoint.class));
} }
} }
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.trace;
import java.io.IOException;
import java.security.Principal;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Servlet {@link Filter} that logs all requests to a {@link TraceRepository}.
*
* @author Dave Syer
* @author Wallace Wadge
* @author Andy Wilkinson
* @author Venil Noronha
* @author Madhura Bhave
*/
public class WebRequestTraceFilter extends OncePerRequestFilter implements Ordered {
private static final Log logger = LogFactory.getLog(WebRequestTraceFilter.class);
private boolean dumpRequests = false;
// Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all
// enriched headers, but users can add stuff after this if they want to
private int order = Ordered.LOWEST_PRECEDENCE - 10;
private final TraceRepository repository;
private ErrorAttributes errorAttributes;
private final Set<Include> includes;
/**
* Create a new {@link WebRequestTraceFilter} instance.
* @param repository the trace repository
* @param includes the {@link Include} to apply
*/
public WebRequestTraceFilter(TraceRepository repository, Set<Include> includes) {
this.repository = repository;
this.includes = includes;
}
/**
* Create a new {@link WebRequestTraceFilter} instance with the default
* {@link Include} to apply.
* @param repository the trace repository
* @see Include#defaultIncludes()
*/
public WebRequestTraceFilter(TraceRepository repository) {
this(repository, Include.defaultIncludes());
}
/**
* Debugging feature. If enabled, and trace logging is enabled then web request
* headers will be logged.
* @param dumpRequests if requests should be logged
*/
public void setDumpRequests(boolean dumpRequests) {
this.dumpRequests = dumpRequests;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
long startTime = System.nanoTime();
Map<String, Object> trace = getTrace(request);
logTrace(request, trace);
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
try {
filterChain.doFilter(request, response);
status = response.getStatus();
}
finally {
addTimeTaken(trace, startTime);
enhanceTrace(trace, status == response.getStatus() ? response
: new CustomStatusResponseWrapper(response, status));
this.repository.add(trace);
}
}
protected Map<String, Object> getTrace(HttpServletRequest request) {
HttpSession session = request.getSession(false);
Throwable exception = (Throwable) request
.getAttribute("javax.servlet.error.exception");
Principal userPrincipal = request.getUserPrincipal();
Map<String, Object> trace = new LinkedHashMap<>();
Map<String, Object> headers = new LinkedHashMap<>();
trace.put("method", request.getMethod());
trace.put("path", request.getRequestURI());
trace.put("headers", headers);
if (isIncluded(Include.REQUEST_HEADERS)) {
headers.put("request", getRequestHeaders(request));
}
add(trace, Include.PATH_INFO, "pathInfo", request.getPathInfo());
add(trace, Include.PATH_TRANSLATED, "pathTranslated",
request.getPathTranslated());
add(trace, Include.CONTEXT_PATH, "contextPath", request.getContextPath());
add(trace, Include.USER_PRINCIPAL, "userPrincipal",
(userPrincipal == null ? null : userPrincipal.getName()));
if (isIncluded(Include.PARAMETERS)) {
trace.put("parameters", getParameterMapCopy(request));
}
add(trace, Include.QUERY_STRING, "query", request.getQueryString());
add(trace, Include.AUTH_TYPE, "authType", request.getAuthType());
add(trace, Include.REMOTE_ADDRESS, "remoteAddress", request.getRemoteAddr());
add(trace, Include.SESSION_ID, "sessionId",
(session == null ? null : session.getId()));
add(trace, Include.REMOTE_USER, "remoteUser", request.getRemoteUser());
if (isIncluded(Include.ERRORS) && exception != null
&& this.errorAttributes != null) {
trace.put("error", this.errorAttributes
.getErrorAttributes(new ServletWebRequest(request), true));
}
return trace;
}
private Map<String, Object> getRequestHeaders(HttpServletRequest request) {
Map<String, Object> headers = new LinkedHashMap<>();
Set<String> excludedHeaders = getExcludeHeaders();
Enumeration<String> names = request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
if (!excludedHeaders.contains(name.toLowerCase())) {
headers.put(name, getHeaderValue(request, name));
}
}
postProcessRequestHeaders(headers);
return headers;
}
private Set<String> getExcludeHeaders() {
Set<String> excludedHeaders = new HashSet<>();
if (!isIncluded(Include.COOKIES)) {
excludedHeaders.add("cookie");
}
if (!isIncluded(Include.AUTHORIZATION_HEADER)) {
excludedHeaders.add("authorization");
}
return excludedHeaders;
}
private Object getHeaderValue(HttpServletRequest request, String name) {
List<String> value = Collections.list(request.getHeaders(name));
if (value.size() == 1) {
return value.get(0);
}
if (value.isEmpty()) {
return "";
}
return value;
}
private Map<String, String[]> getParameterMapCopy(HttpServletRequest request) {
return new LinkedHashMap<>(request.getParameterMap());
}
/**
* Post process request headers before they are added to the trace.
* @param headers a mutable map containing the request headers to trace
* @since 1.4.0
*/
protected void postProcessRequestHeaders(Map<String, Object> headers) {
}
private void addTimeTaken(Map<String, Object> trace, long startTime) {
long timeTaken = System.nanoTime() - startTime;
add(trace, Include.TIME_TAKEN, "timeTaken",
String.valueOf(TimeUnit.NANOSECONDS.toMillis(timeTaken)));
}
@SuppressWarnings("unchecked")
protected void enhanceTrace(Map<String, Object> trace, HttpServletResponse response) {
if (isIncluded(Include.RESPONSE_HEADERS)) {
Map<String, Object> headers = (Map<String, Object>) trace.get("headers");
headers.put("response", getResponseHeaders(response));
}
}
private Map<String, String> getResponseHeaders(HttpServletResponse response) {
Map<String, String> headers = new LinkedHashMap<>();
for (String header : response.getHeaderNames()) {
String value = response.getHeader(header);
headers.put(header, value);
}
if (!isIncluded(Include.COOKIES)) {
headers.remove("Set-Cookie");
}
headers.put("status", String.valueOf(response.getStatus()));
return headers;
}
private void logTrace(HttpServletRequest request, Map<String, Object> trace) {
if (logger.isTraceEnabled()) {
logger.trace("Processing request " + request.getMethod() + " "
+ request.getRequestURI());
if (this.dumpRequests) {
logger.trace("Headers: " + trace.get("headers"));
}
}
}
private void add(Map<String, Object> trace, Include include, String name,
Object value) {
if (isIncluded(include) && value != null) {
trace.put(name, value);
}
}
private boolean isIncluded(Include include) {
return this.includes.contains(include);
}
public void setErrorAttributes(ErrorAttributes errorAttributes) {
this.errorAttributes = errorAttributes;
}
private static final class CustomStatusResponseWrapper
extends HttpServletResponseWrapper {
private final int status;
private CustomStatusResponseWrapper(HttpServletResponse response, int status) {
super(response);
this.status = status;
}
@Override
public int getStatus() {
return this.status;
}
}
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.web.trace;
import java.net.URI;
import java.security.Principal;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.springframework.http.HttpHeaders;
/**
* Traces an HTTP request-response exchange.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class HttpExchangeTracer {
private final Set<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 the concludes the exchange
* @param principal a supplier for the exchange's principal
* @param sessionId a supplier for the id of the exchange's session
*/
public final void sendingResponse(HttpTrace trace, TraceableResponse response,
Supplier<Principal> principal, Supplier<String> sessionId) {
setIfIncluded(Include.TIME_TAKEN,
() -> System.currentTimeMillis() - trace.getTimestamp().toEpochMilli(),
trace::setTimeTaken);
setIfIncluded(Include.SESSION_ID, sessionId, trace::setSessionId);
setIfIncluded(Include.PRINCIPAL, principal, trace::setPrincipal);
trace.setResponse(
new HttpTrace.Response(new FilteredTraceableResponse(response)));
}
/**
* Post-process the given mutable map of request {@code headers}.
* @param headers the headers to post-process
*/
protected void postProcessRequestHeaders(Map<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<>();
}
Map<String, List<String>> headers = headersSupplier.get();
Iterator<String> keys = headers.keySet().iterator();
while (keys.hasNext()) {
if (!headerPredicate.test(keys.next())) {
keys.remove();
}
}
return headers;
}
private final class FilteredTraceableRequest implements TraceableRequest {
private final TraceableRequest delegate;
private FilteredTraceableRequest(TraceableRequest delegate) {
this.delegate = delegate;
}
@Override
public String getMethod() {
return this.delegate.getMethod();
}
@Override
public URI getUri() {
return this.delegate.getUri();
}
@Override
public Map<String, List<String>> getHeaders() {
return getHeadersIfIncluded(Include.REQUEST_HEADERS,
this.delegate::getHeaders, (name) -> {
if (name.equalsIgnoreCase(HttpHeaders.COOKIE)) {
return HttpExchangeTracer.this.includes
.contains(Include.COOKIE_HEADERS);
}
if (name.equalsIgnoreCase(HttpHeaders.AUTHORIZATION)) {
return HttpExchangeTracer.this.includes
.contains(Include.AUTHORIZATION_HEADER);
}
return true;
});
}
@Override
public String getRemoteAddress() {
return getIfIncluded(Include.REMOTE_ADDRESS, this.delegate::getRemoteAddress);
}
}
private final class FilteredTraceableResponse implements TraceableResponse {
private final TraceableResponse delegate;
private FilteredTraceableResponse(TraceableResponse delegate) {
this.delegate = delegate;
}
@Override
public int getStatus() {
return this.delegate.getStatus();
}
@Override
public Map<String, List<String>> getHeaders() {
return getHeadersIfIncluded(Include.RESPONSE_HEADERS,
this.delegate::getHeaders, (name) -> {
if (name.equalsIgnoreCase(HttpHeaders.SET_COOKIE)) {
return HttpExchangeTracer.this.includes
.contains(Include.COOKIE_HEADERS);
}
return true;
});
}
}
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.web.trace;
import java.net.URI;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import org.springframework.util.StringUtils;
/**
* A trace event for handling of an HTTP request and response exchange. Can be used for
* analyzing contextual information such as HTTP headers.
*
* @author Dave Syer
* @author Andy Wilkinson
* @since 2.0.0
*/
public final class HttpTrace {
private final Instant timestamp = Instant.now();
private volatile Principal principal;
private volatile Session session;
private final Request request;
private volatile Response response;
private volatile Long timeTaken;
HttpTrace(TraceableRequest request) {
this.request = new Request(request);
}
public Instant getTimestamp() {
return this.timestamp;
}
void setPrincipal(java.security.Principal principal) {
if (principal != null) {
this.principal = new Principal(principal.getName());
}
}
public Principal getPrincipal() {
return this.principal;
}
public Session getSession() {
return this.session;
}
void setSessionId(String sessionId) {
if (StringUtils.hasText(sessionId)) {
this.session = new Session(sessionId);
}
}
public Request getRequest() {
return this.request;
}
public Response getResponse() {
return this.response;
}
void setResponse(Response response) {
this.response = response;
}
public Long getTimeTaken() {
return this.timeTaken;
}
void setTimeTaken(long timeTaken) {
this.timeTaken = timeTaken;
}
/**
* Trace of an HTTP request.
*/
public static final class Request {
private final String method;
private final URI uri;
private final Map<String, List<String>> headers;
private final String remoteAddress;
private Request(TraceableRequest request) {
this.method = request.getMethod();
this.uri = request.getUri();
this.headers = request.getHeaders();
this.remoteAddress = request.getRemoteAddress();
}
public String getMethod() {
return this.method;
}
public URI getUri() {
return this.uri;
}
public Map<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.status = response.getStatus();
this.headers = response.getHeaders();
}
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;
private Session(String id) {
this.id = id;
}
public String getId() {
return this.id;
}
}
/**
* Principal associated with an HTTP request-response exchange.
*/
public static final class Principal {
private final String name;
private Principal(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
}
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.trace; package org.springframework.boot.actuate.web.trace;
import java.util.List; import java.util.List;
...@@ -23,21 +23,21 @@ import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; ...@@ -23,21 +23,21 @@ import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* {@link Endpoint} to expose {@link Trace} information. * {@link Endpoint} to expose {@link HttpTrace} information.
* *
* @author Dave Syer * @author Dave Syer
* @since 2.0.0 * @since 2.0.0
*/ */
@Endpoint(id = "trace") @Endpoint(id = "trace")
public class TraceEndpoint { public class HttpTraceEndpoint {
private final TraceRepository repository; private final HttpTraceRepository repository;
/** /**
* Create a new {@link TraceEndpoint} instance. * Create a new {@link HttpTraceEndpoint} instance.
* @param repository the trace repository * @param repository the trace repository
*/ */
public TraceEndpoint(TraceRepository repository) { public HttpTraceEndpoint(HttpTraceRepository repository) {
Assert.notNull(repository, "Repository must not be null"); Assert.notNull(repository, "Repository must not be null");
this.repository = repository; this.repository = repository;
} }
...@@ -48,18 +48,18 @@ public class TraceEndpoint { ...@@ -48,18 +48,18 @@ public class TraceEndpoint {
} }
/** /**
* A description of an application's {@link Trace} entries. Primarily intended for * A description of an application's {@link HttpTrace} entries. Primarily intended for
* serialization to JSON. * serialization to JSON.
*/ */
public static final class TraceDescriptor { public static final class TraceDescriptor {
private final List<Trace> traces; private final List<HttpTrace> traces;
private TraceDescriptor(List<Trace> traces) { private TraceDescriptor(List<HttpTrace> traces) {
this.traces = traces; this.traces = traces;
} }
public List<Trace> getTraces() { public List<HttpTrace> getTraces() {
return this.traces; return this.traces;
} }
......
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -14,28 +14,29 @@ ...@@ -14,28 +14,29 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.trace; package org.springframework.boot.actuate.web.trace;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* A repository for {@link Trace}s. * A repository for {@link HttpTrace}s.
* *
* @author Dave Syer * @author Dave Syer
* @author Andy Wilkinson
* @since 2.0.0
*/ */
public interface TraceRepository { public interface HttpTraceRepository {
/** /**
* Find all {@link Trace} objects contained in the repository. * Find all {@link HttpTrace} objects contained in the repository.
* @return the results * @return the results
*/ */
List<Trace> findAll(); List<HttpTrace> findAll();
/** /**
* Add a new {@link Trace} object at the current time. * Adds a trace to the repository.
* @param traceInfo trace information * @param trace the trace to add
*/ */
void add(Map<String, Object> traceInfo); void add(HttpTrace trace);
} }
...@@ -14,28 +14,27 @@ ...@@ -14,28 +14,27 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.trace; package org.springframework.boot.actuate.web.trace;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* In-memory implementation of {@link TraceRepository}. * In-memory implementation of {@link HttpTraceRepository}.
* *
* @author Dave Syer * @author Dave Syer
* @author Olivier Bourgain * @author Olivier Bourgain
* @since 2.0.0
*/ */
public class InMemoryTraceRepository implements TraceRepository { public class InMemoryHttpTraceRepository implements HttpTraceRepository {
private int capacity = 100; private int capacity = 100;
private boolean reverse = true; private boolean reverse = true;
private final List<Trace> traces = new LinkedList<>(); private final List<HttpTrace> traces = new LinkedList<>();
/** /**
* Flag to say that the repository lists traces in reverse order. * Flag to say that the repository lists traces in reverse order.
...@@ -58,15 +57,14 @@ public class InMemoryTraceRepository implements TraceRepository { ...@@ -58,15 +57,14 @@ public class InMemoryTraceRepository implements TraceRepository {
} }
@Override @Override
public List<Trace> findAll() { public List<HttpTrace> findAll() {
synchronized (this.traces) { synchronized (this.traces) {
return Collections.unmodifiableList(new ArrayList<>(this.traces)); return Collections.unmodifiableList(new ArrayList<>(this.traces));
} }
} }
@Override @Override
public void add(Map<String, Object> map) { public void add(HttpTrace trace) {
Trace trace = new Trace(Instant.now(), map);
synchronized (this.traces) { synchronized (this.traces) {
while (this.traces.size() >= this.capacity) { while (this.traces.size() >= this.capacity) {
this.traces.remove(this.reverse ? this.capacity - 1 : 0); this.traces.remove(this.reverse ? this.capacity - 1 : 0);
......
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -14,14 +14,14 @@ ...@@ -14,14 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.trace; package org.springframework.boot.actuate.web.trace;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
/** /**
* Include options for tracing. * Include options for HTTP tracing.
* *
* @author Wallace Wadge * @author Wallace Wadge
* @since 2.0.0 * @since 2.0.0
...@@ -39,9 +39,10 @@ public enum Include { ...@@ -39,9 +39,10 @@ public enum Include {
RESPONSE_HEADERS, RESPONSE_HEADERS,
/** /**
* Include "Cookie" in request and "Set-Cookie" in response headers. * Include "Cookie" header (if any) in request headers and "Set-Cookie" (if any) in
* response headers.
*/ */
COOKIES, COOKIE_HEADERS,
/** /**
* Include authorization header (if any). * Include authorization header (if any).
...@@ -49,44 +50,9 @@ public enum Include { ...@@ -49,44 +50,9 @@ public enum Include {
AUTHORIZATION_HEADER, AUTHORIZATION_HEADER,
/** /**
* Include errors (if any). * Include the principal.
*/ */
ERRORS, PRINCIPAL,
/**
* Include path info.
*/
PATH_INFO,
/**
* Include the translated path.
*/
PATH_TRANSLATED,
/**
* Include the context path.
*/
CONTEXT_PATH,
/**
* Include the user principal.
*/
USER_PRINCIPAL,
/**
* Include the parameters.
*/
PARAMETERS,
/**
* Include the query string.
*/
QUERY_STRING,
/**
* Include the authentication type.
*/
AUTH_TYPE,
/** /**
* Include the remote address. * Include the remote address.
...@@ -98,11 +64,6 @@ public enum Include { ...@@ -98,11 +64,6 @@ public enum Include {
*/ */
SESSION_ID, SESSION_ID,
/**
* Include the remote user.
*/
REMOTE_USER,
/** /**
* Include the time taken to service the request in milliseconds. * Include the time taken to service the request in milliseconds.
*/ */
...@@ -114,8 +75,7 @@ public enum Include { ...@@ -114,8 +75,7 @@ public enum Include {
Set<Include> defaultIncludes = new LinkedHashSet<>(); Set<Include> defaultIncludes = new LinkedHashSet<>();
defaultIncludes.add(Include.REQUEST_HEADERS); defaultIncludes.add(Include.REQUEST_HEADERS);
defaultIncludes.add(Include.RESPONSE_HEADERS); defaultIncludes.add(Include.RESPONSE_HEADERS);
defaultIncludes.add(Include.COOKIES); defaultIncludes.add(Include.COOKIE_HEADERS);
defaultIncludes.add(Include.ERRORS);
defaultIncludes.add(Include.TIME_TAKEN); defaultIncludes.add(Include.TIME_TAKEN);
DEFAULT_INCLUDES = Collections.unmodifiableSet(defaultIncludes); DEFAULT_INCLUDES = Collections.unmodifiableSet(defaultIncludes);
} }
......
...@@ -14,38 +14,43 @@ ...@@ -14,38 +14,43 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.trace; package org.springframework.boot.actuate.web.trace;
import java.time.Instant; import java.net.URI;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.util.Assert;
/** /**
* A value object representing a trace event: at a particular time with a simple (map) * A representation of an HTTP request that is suitable for tracing.
* information. Can be used for analyzing contextual information such as HTTP headers.
* *
* @author Dave Syer * @author Andy Wilkinson
* @since 2.0.0
* @see HttpExchangeTracer
*/ */
public final class Trace { public interface TraceableRequest {
private final Instant timestamp; /**
* Returns the method (GET, POST, etc) of the request.
private final Map<String, Object> info; * @return the method
*/
public Trace(Instant timestamp, Map<String, Object> info) { String getMethod();
Assert.notNull(timestamp, "Timestamp must not be null");
Assert.notNull(info, "Info must not be null"); /**
this.timestamp = timestamp; * Returns the URI of the request.
this.info = info; * @return the URI
} */
URI getUri();
public Instant getTimestamp() {
return this.timestamp; /**
} * Returns a modifiable copy of the headers of the request.
* @return the headers
public Map<String, Object> getInfo() { */
return this.info; Map<String, List<String>> getHeaders();
}
/**
* Returns the remote address from which the request was sent, if available.
* @return the remote address or {@code null}
*/
String getRemoteAddress();
} }
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -14,28 +14,30 @@ ...@@ -14,28 +14,30 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.trace; package org.springframework.boot.actuate.web.trace;
import java.util.Collections; import java.util.List;
import java.util.Map;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Tests for {@link TraceEndpoint}. * A representation of an HTTP response that is suitable for tracing.
* *
* @author Phillip Webb
* @author Andy Wilkinson * @author Andy Wilkinson
* @since 2.0.0
* @see HttpExchangeTracer
*/ */
public class TraceEndpointTests { public interface TraceableResponse {
/**
* The status of the response.
* @return the status
*/
int getStatus();
@Test /**
public void trace() { * Returns a modifiable copy of the headers of the response.
TraceRepository repository = new InMemoryTraceRepository(); * @return the headers
repository.add(Collections.singletonMap("a", "b")); */
Trace trace = new TraceEndpoint(repository).traces().getTraces().get(0); Map<String, List<String>> getHeaders();
assertThat(trace.getInfo().get("a")).isEqualTo("b");
}
} }
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -15,8 +15,8 @@ ...@@ -15,8 +15,8 @@
*/ */
/** /**
* Actuator tracing support. * Actuator HTTP tracing support.
* *
* @see org.springframework.boot.actuate.trace.TraceRepository * @see org.springframework.boot.actuate.web.trace.HttpTraceRepository
*/ */
package org.springframework.boot.actuate.trace; package org.springframework.boot.actuate.web.trace;
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.web.trace.reactive;
import java.security.Principal;
import java.util.Set;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
import org.springframework.boot.actuate.web.trace.HttpTrace;
import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
import org.springframework.boot.actuate.web.trace.Include;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebSession;
/**
* A {@link WebFilter} for tracing HTTP requests.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class HttpTraceWebFilter implements WebFilter, Ordered {
private static final Object NONE = new Object();
// Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all
// enriched headers, but users can add stuff after this if they want to
private int order = Ordered.LOWEST_PRECEDENCE - 10;
private final HttpTraceRepository repository;
private final HttpExchangeTracer tracer;
private final Set<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);
return chain.filter(exchange).doAfterSuccessOrError((aVoid, ex) -> {
this.tracer.sendingResponse(trace,
new TraceableServerHttpResponse(ex == null ? exchange.getResponse()
: new CustomStatusResponseDecorator(ex,
exchange.getResponse())),
() -> principal, () -> getStartedSessionId(session));
this.repository.add(trace);
});
}
private String getStartedSessionId(WebSession session) {
return (session != null && session.isStarted()) ? session.getId() : null;
}
private static final class CustomStatusResponseDecorator
extends ServerHttpResponseDecorator {
private final HttpStatus status;
private CustomStatusResponseDecorator(Throwable ex, ServerHttpResponse delegate) {
super(delegate);
this.status = ex instanceof ResponseStatusException
? ((ResponseStatusException) ex).getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
}
@Override
public HttpStatus getStatusCode() {
return this.status;
}
}
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.web.trace.reactive;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.actuate.web.trace.TraceableRequest;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
/**
* A {@link TraceableRequest} backed by a {@link ServerWebExchange}.
*
* @author Andy Wilkinson
*/
class ServerWebExchangeTraceableRequest implements TraceableRequest {
private final String method;
private final Map<String, List<String>> headers;
private final URI uri;
private final String remoteAddress;
ServerWebExchangeTraceableRequest(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
this.method = request.getMethodValue();
this.headers = request.getHeaders();
this.uri = request.getURI();
this.remoteAddress = request.getRemoteAddress() == null ? null
: request.getRemoteAddress().getAddress().toString();
}
@Override
public String getMethod() {
return this.method;
}
@Override
public URI getUri() {
return this.uri;
}
@Override
public Map<String, List<String>> getHeaders() {
return new LinkedHashMap<>(this.headers);
}
@Override
public String getRemoteAddress() {
return this.remoteAddress;
}
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.web.trace.reactive;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.actuate.web.trace.TraceableResponse;
import org.springframework.http.server.reactive.ServerHttpResponse;
/**
* An adapter that exposes a {@link ServerHttpResponse} as a {@link TraceableResponse}.
*
* @author Andy Wilkinson
*/
class TraceableServerHttpResponse implements TraceableResponse {
private final ServerHttpResponse response;
TraceableServerHttpResponse(ServerHttpResponse exchange) {
this.response = exchange;
}
@Override
public int getStatus() {
return this.response.getStatusCode() == null ? 200
: this.response.getStatusCode().value();
}
@Override
public Map<String, List<String>> getHeaders() {
return new LinkedHashMap<>(this.response.getHeaders());
}
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Actuator reactive HTTP tracing support.
*
* @see org.springframework.boot.actuate.web.trace.HttpTraceRepository
*/
package org.springframework.boot.actuate.web.trace.reactive;
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.web.trace.servlet;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
import org.springframework.boot.actuate.web.trace.HttpTrace;
import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Servlet {@link Filter} that logs all requests to an {@link HttpTraceRepository}.
*
* @author Dave Syer
* @author Wallace Wadge
* @author Andy Wilkinson
* @author Venil Noronha
* @author Madhura Bhave
* @since 2.0.0
*/
public class HttpTraceFilter extends OncePerRequestFilter implements Ordered {
// Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all
// enriched headers, but users can add stuff after this if they want to
private int order = Ordered.LOWEST_PRECEDENCE - 10;
private final HttpTraceRepository repository;
private final HttpExchangeTracer tracer;
/**
* Create a new {@link HttpTraceFilter} instance.
* @param repository the trace repository
* @param tracer used to trace exchanges
*/
public HttpTraceFilter(HttpTraceRepository repository, HttpExchangeTracer tracer) {
this.repository = repository;
this.tracer = tracer;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
TraceableHttpServletRequest traceableRequest = new TraceableHttpServletRequest(
request);
HttpTrace trace = this.tracer.receivedRequest(traceableRequest);
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
try {
filterChain.doFilter(request, response);
status = response.getStatus();
}
finally {
TraceableHttpServletResponse traceableResponse = new TraceableHttpServletResponse(
status == response.getStatus() ? response
: new CustomStatusResponseWrapper(response, status));
this.tracer.sendingResponse(trace, traceableResponse,
request::getUserPrincipal, () -> getSessionId(request));
this.repository.add(trace);
}
}
private String getSessionId(HttpServletRequest request) {
HttpSession session = request.getSession(false);
return session == null ? null : session.getId();
}
private static final class CustomStatusResponseWrapper
extends HttpServletResponseWrapper {
private final int status;
private CustomStatusResponseWrapper(HttpServletResponse response, int status) {
super(response);
this.status = status;
}
@Override
public int getStatus() {
return this.status;
}
}
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.web.trace.servlet;
import java.net.URI;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.actuate.web.trace.TraceableRequest;
import org.springframework.util.StringUtils;
/**
* An adapter that exposes an {@link HttpServletRequest} as a {@link TraceableRequest}.
*
* @author Andy Wilkinson
*/
final class TraceableHttpServletRequest implements TraceableRequest {
private final HttpServletRequest request;
TraceableHttpServletRequest(HttpServletRequest request) {
this.request = request;
}
@Override
public String getMethod() {
return this.request.getMethod();
}
@Override
public URI getUri() {
StringBuffer urlBuffer = this.request.getRequestURL();
if (StringUtils.hasText(this.request.getQueryString())) {
urlBuffer.append("?");
urlBuffer.append(this.request.getQueryString());
}
return URI.create(urlBuffer.toString());
}
@Override
public Map<String, List<String>> getHeaders() {
return extractHeaders();
}
@Override
public String getRemoteAddress() {
return this.request.getRemoteAddr();
}
private Map<String, List<String>> extractHeaders() {
Map<String, List<String>> headers = new LinkedHashMap<>();
Enumeration<String> names = this.request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
headers.put(name, toList(this.request.getHeaders(name)));
}
return headers;
}
private List<String> toList(Enumeration<String> enumeration) {
List<String> list = new ArrayList<String>();
while (enumeration.hasMoreElements()) {
list.add(enumeration.nextElement());
}
return list;
}
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.web.trace.servlet;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.actuate.web.trace.TraceableResponse;
/**
* An adapter that exposes an {@link HttpServletResponse} as a {@link TraceableResponse}.
*
* @author Andy Wilkinson
*/
final class TraceableHttpServletResponse implements TraceableResponse {
private final HttpServletResponse delegate;
TraceableHttpServletResponse(HttpServletResponse response) {
this.delegate = response;
}
@Override
public int getStatus() {
return this.delegate.getStatus();
}
@Override
public Map<String, List<String>> getHeaders() {
return extractHeaders();
}
private Map<String, List<String>> extractHeaders() {
Map<String, List<String>> headers = new LinkedHashMap<>();
for (String name : this.delegate.getHeaderNames()) {
headers.put(name, new ArrayList<>(this.delegate.getHeaders(name)));
}
return headers;
}
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Actuator servlet HTTP tracing support.
*
* @see org.springframework.boot.actuate.web.trace.HttpTraceRepository
*/
package org.springframework.boot.actuate.web.trace.servlet;
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -14,47 +14,55 @@ ...@@ -14,47 +14,55 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.trace; package org.springframework.boot.actuate.web.trace;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.Test; import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link InMemoryTraceRepository}. * Tests for {@link InMemoryHttpTraceRepository}.
* *
* @author Dave Syer * @author Dave Syer
* @author Andy Wilkinson
*/ */
public class InMemoryTraceRepositoryTests { public class InMemoryTraceRepositoryTests {
private final InMemoryTraceRepository repository = new InMemoryTraceRepository(); private final InMemoryHttpTraceRepository repository = new InMemoryHttpTraceRepository();
@Test @Test
public void capacityLimited() { public void capacityLimited() {
this.repository.setCapacity(2); this.repository.setCapacity(2);
this.repository.add(Collections.singletonMap("foo", "bar")); this.repository.add(new HttpTrace(createRequest("GET")));
this.repository.add(Collections.singletonMap("bar", "foo")); this.repository.add(new HttpTrace(createRequest("POST")));
this.repository.add(Collections.singletonMap("bar", "bar")); this.repository.add(new HttpTrace(createRequest("DELETE")));
List<Trace> traces = this.repository.findAll(); List<HttpTrace> traces = this.repository.findAll();
assertThat(traces).hasSize(2); assertThat(traces).hasSize(2);
assertThat(traces.get(0).getInfo().get("bar")).isEqualTo("bar"); assertThat(traces.get(0).getRequest().getMethod()).isEqualTo("DELETE");
assertThat(traces.get(1).getInfo().get("bar")).isEqualTo("foo"); assertThat(traces.get(1).getRequest().getMethod()).isEqualTo("POST");
} }
@Test @Test
public void reverseFalse() { public void reverseFalse() {
this.repository.setReverse(false); this.repository.setReverse(false);
this.repository.setCapacity(2); this.repository.setCapacity(2);
this.repository.add(Collections.singletonMap("foo", "bar")); this.repository.add(new HttpTrace(createRequest("GET")));
this.repository.add(Collections.singletonMap("bar", "foo")); this.repository.add(new HttpTrace(createRequest("POST")));
this.repository.add(Collections.singletonMap("bar", "bar")); this.repository.add(new HttpTrace(createRequest("DELETE")));
List<Trace> traces = this.repository.findAll(); List<HttpTrace> traces = this.repository.findAll();
assertThat(traces).hasSize(2); assertThat(traces).hasSize(2);
assertThat(traces.get(1).getInfo().get("bar")).isEqualTo("bar"); assertThat(traces.get(0).getRequest().getMethod()).isEqualTo("POST");
assertThat(traces.get(0).getInfo().get("bar")).isEqualTo("foo"); assertThat(traces.get(1).getRequest().getMethod()).isEqualTo("DELETE");
}
private TraceableRequest createRequest(String method) {
TraceableRequest request = mock(TraceableRequest.class);
given(request.getMethod()).willReturn(method);
return request;
} }
} }
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.web.trace;
import java.util.List;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link HttpTraceEndpoint}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class TraceEndpointTests {
@Test
public void trace() {
HttpTraceRepository repository = new InMemoryHttpTraceRepository();
repository.add(new HttpTrace(createRequest("GET")));
List<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;
}
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.web.trace.reactive;
import java.util.EnumSet;
import java.util.Set;
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository;
import org.springframework.boot.actuate.web.trace.Include;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link HttpTraceWebFilter}.
*
* @author Andy Wilkinson
*/
public class HttpTraceWebFilterIntegrationTests {
@Test
public void traceForNotFoundResponseHas404Status() {
ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner()
.withUserConfiguration(Config.class);
runner.run((context) -> {
WebTestClient.bindToApplicationContext(context).build().get().uri("/")
.exchange().expectStatus().isNotFound();
HttpTraceRepository repository = context.getBean(HttpTraceRepository.class);
assertThat(repository.findAll()).hasSize(1);
assertThat(repository.findAll().get(0).getResponse().getStatus())
.isEqualTo(404);
});
}
@Test
public void traceForMonoErrorWithRuntimeExceptionHas500Status() {
ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner()
.withUserConfiguration(Config.class);
runner.run((context) -> {
WebTestClient.bindToApplicationContext(context).build().get()
.uri("/mono-error").exchange().expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
HttpTraceRepository repository = context.getBean(HttpTraceRepository.class);
assertThat(repository.findAll()).hasSize(1);
assertThat(repository.findAll().get(0).getResponse().getStatus())
.isEqualTo(500);
});
}
@Test
public void traceForThrownRuntimeExceptionHas500Status() {
ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner()
.withUserConfiguration(Config.class);
runner.run((context) -> {
WebTestClient.bindToApplicationContext(context).build().get().uri("/thrown")
.exchange().expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
HttpTraceRepository repository = context.getBean(HttpTraceRepository.class);
assertThat(repository.findAll()).hasSize(1);
assertThat(repository.findAll().get(0).getResponse().getStatus())
.isEqualTo(500);
});
}
@Configuration
@EnableWebFlux
static class Config {
@Bean
public HttpTraceWebFilter httpTraceWebFilter(HttpTraceRepository repository) {
Set<Include> includes = EnumSet.allOf(Include.class);
return new HttpTraceWebFilter(repository, new HttpExchangeTracer(includes),
includes);
}
@Bean
public HttpTraceRepository httpTraceRepository() {
return new InMemoryHttpTraceRepository();
}
@Bean
public HttpHandler httpHandler(ApplicationContext applicationContext) {
return WebHttpHandlerBuilder.applicationContext(applicationContext).build();
}
@Bean
public RouterFunction<ServerResponse> router() {
return RouterFunctions
.route(RequestPredicates.GET("/mono-error"),
(request) -> Mono.error(new RuntimeException()))
.andRoute(RequestPredicates.GET("/thrown"),
(HandlerFunction<ServerResponse>) (request) -> {
throw new RuntimeException();
});
}
}
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.web.trace.reactive;
import java.io.IOException;
import java.security.Principal;
import java.util.EnumSet;
import javax.servlet.ServletException;
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
import org.springframework.boot.actuate.web.trace.HttpTrace.Session;
import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository;
import org.springframework.boot.actuate.web.trace.Include;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.WebFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link HttpTraceWebFilter}.
*
* @author Andy Wilkinson
*/
public class HttpTraceWebFilterTests {
private final InMemoryHttpTraceRepository repository = new InMemoryHttpTraceRepository();
private final HttpExchangeTracer tracer = new HttpExchangeTracer(
EnumSet.allOf(Include.class));
private final HttpTraceWebFilter filter = new HttpTraceWebFilter(this.repository,
this.tracer, EnumSet.allOf(Include.class));
@Test
public void filterTracesExchange() throws ServletException, IOException {
this.filter.filter(
MockServerWebExchange
.from(MockServerHttpRequest.get("https://api.example.com")),
new WebFilterChain() {
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.empty();
}
}).block();
assertThat(this.repository.findAll()).hasSize(1);
}
@Test
public void filterCapturesSessionIdWhenSessionIsUsed()
throws ServletException, IOException {
this.filter.filter(
MockServerWebExchange
.from(MockServerHttpRequest.get("https://api.example.com")),
new WebFilterChain() {
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
exchange.getSession().block().getAttributes().put("a", "alpha");
return Mono.empty();
}
}).block();
assertThat(this.repository.findAll()).hasSize(1);
Session session = this.repository.findAll().get(0).getSession();
assertThat(session).isNotNull();
assertThat(session.getId()).isNotNull();
}
@Test
public void filterDoesNotCaptureIdOfUnusedSession()
throws ServletException, IOException {
this.filter.filter(
MockServerWebExchange
.from(MockServerHttpRequest.get("https://api.example.com")),
new WebFilterChain() {
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
exchange.getSession().block();
return Mono.empty();
}
}).block();
assertThat(this.repository.findAll()).hasSize(1);
Session session = this.repository.findAll().get(0).getSession();
assertThat(session).isNull();
}
@Test
public void filterCapturesPrincipal() throws ServletException, IOException {
Principal principal = mock(Principal.class);
given(principal.getName()).willReturn("alice");
this.filter.filter(new ServerWebExchangeDecorator(MockServerWebExchange
.from(MockServerHttpRequest.get("https://api.example.com"))) {
@Override
public Mono<Principal> getPrincipal() {
return Mono.just(principal);
}
}, new WebFilterChain() {
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
exchange.getSession().block().getAttributes().put("a", "alpha");
return Mono.empty();
}
}).block();
assertThat(this.repository.findAll()).hasSize(1);
org.springframework.boot.actuate.web.trace.HttpTrace.Principal tracedPrincipal = this.repository
.findAll().get(0).getPrincipal();
assertThat(tracedPrincipal).isNotNull();
assertThat(tracedPrincipal.getName()).isEqualTo("alice");
}
@Test
public void statusIsAssumedToBe500WhenChainFails()
throws ServletException, IOException {
try {
this.filter.filter(
MockServerWebExchange
.from(MockServerHttpRequest.get("https://api.example.com")),
new WebFilterChain() {
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.error(new RuntimeException());
}
}).block();
fail();
}
catch (Exception ex) {
assertThat(this.repository.findAll()).hasSize(1);
assertThat(this.repository.findAll().get(0).getResponse().getStatus())
.isEqualTo(500);
}
}
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.web.trace.servlet;
import java.io.IOException;
import java.security.Principal;
import java.util.EnumSet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Test;
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
import org.springframework.boot.actuate.web.trace.HttpTrace.Session;
import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository;
import org.springframework.boot.actuate.web.trace.Include;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link HttpTraceFilter}.
*
* @author Dave Syer
* @author Wallace Wadge
* @author Phillip Webb
* @author Andy Wilkinson
* @author Venil Noronha
* @author Stephane Nicoll
* @author Madhura Bhave
*/
public class HttpTraceFilterTests {
private final InMemoryHttpTraceRepository repository = new InMemoryHttpTraceRepository();
private final HttpExchangeTracer tracer = new HttpExchangeTracer(
EnumSet.allOf(Include.class));
private final HttpTraceFilter filter = new HttpTraceFilter(this.repository,
this.tracer);
@Test
public void filterTracesExchange() throws ServletException, IOException {
this.filter.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(),
new MockFilterChain());
assertThat(this.repository.findAll()).hasSize(1);
}
@Test
public void filterCapturesSessionId() throws ServletException, IOException {
this.filter.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(),
new MockFilterChain(new HttpServlet() {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
req.getSession(true);
}
}));
assertThat(this.repository.findAll()).hasSize(1);
Session session = this.repository.findAll().get(0).getSession();
assertThat(session).isNotNull();
assertThat(session.getId()).isNotNull();
}
@Test
public void filterCapturesPrincipal() throws ServletException, IOException {
MockHttpServletRequest request = new MockHttpServletRequest();
Principal principal = mock(Principal.class);
given(principal.getName()).willReturn("alice");
request.setUserPrincipal(principal);
this.filter.doFilter(request, new MockHttpServletResponse(),
new MockFilterChain());
assertThat(this.repository.findAll()).hasSize(1);
org.springframework.boot.actuate.web.trace.HttpTrace.Principal tracedPrincipal = this.repository
.findAll().get(0).getPrincipal();
assertThat(tracedPrincipal).isNotNull();
assertThat(tracedPrincipal.getName()).isEqualTo("alice");
}
@Test
public void statusIsAssumedToBe500WhenChainFails()
throws ServletException, IOException {
try {
this.filter.doFilter(new MockHttpServletRequest(),
new MockHttpServletResponse(), new MockFilterChain(new HttpServlet() {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
throw new IOException();
}
}));
fail("Filter swallowed IOException");
}
catch (IOException ex) {
assertThat(this.repository.findAll()).hasSize(1);
assertThat(this.repository.findAll().get(0).getResponse().getStatus())
.isEqualTo(500);
}
}
}
...@@ -115,7 +115,7 @@ store. Not available when using Spring Session's support for reactive web applic ...@@ -115,7 +115,7 @@ store. Not available when using Spring Session's support for reactive web applic
|Performs a thread dump. |Performs a thread dump.
|`trace` |`trace`
|Displays trace information (by default, the last 100 HTTP requests). |Displays HTTP trace information (by default, the last 100 HTTP requests).
|=== |===
If your application is a web application (Spring MVC, Spring WebFlux, or Jersey), you can If your application is a web application (Spring MVC, Spring WebFlux, or Jersey), you can
...@@ -1161,71 +1161,14 @@ implementing `ApplicationEventPublisherAware`). ...@@ -1161,71 +1161,14 @@ implementing `ApplicationEventPublisherAware`).
[[production-ready-tracing]] [[production-ready-tracing]]
== Tracing == Tracing
Tracing is automatically enabled for all HTTP requests. You can view the `trace` endpoint Tracing is automatically enabled for all HTTP requests. You can view the `trace` endpoint
and obtain basic information about the last 100 requests. The following listing shows and obtain basic information about the last 100 requests.
sample output:
[source,json,indent=0]
----
[{
"timestamp": 1394343677415,
"info": {
"method": "GET",
"path": "/trace",
"headers": {
"request": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Connection": "keep-alive",
"Accept-Encoding": "gzip, deflate",
"User-Agent": "Mozilla/5.0 Gecko/Firefox",
"Accept-Language": "en-US,en;q=0.5",
"Cookie": "_ga=GA1.1.827067509.1390890128; ..."
"Authorization": "Basic ...",
"Host": "localhost:8080"
},
"response": {
"Strict-Transport-Security": "max-age=31536000 ; includeSubDomains",
"X-Application-Context": "application:8080",
"Content-Type": "application/json;charset=UTF-8",
"status": "200"
}
}
}
},{
"timestamp": 1394343684465,
...
}]
----
By default, the trace includes the following information:
[cols="1,2"]
|===
|Name |Description
|Request Headers
|Headers from the request.
|Response Headers
|Headers from the response.
|Cookies
|`Cookie` from request headers and `Set-Cookie` from response headers.
|Errors
|The error attributes (if any).
|Time Taken
|The time taken to service the request in milliseconds.
|===
[[production-ready-custom-tracing]] [[production-ready-custom-tracing]]
=== Custom tracing === Custom tracing
If you need to trace additional events, you can inject a To customize the items that are included in each trace, use the
{sc-spring-boot-actuator}/trace/TraceRepository.{sc-ext}[`TraceRepository`] into your `management.trace.include` configuration property.
Spring beans. The `add` method accepts a single `Map` structure that is converted to JSON
and logged.
By default, an `InMemoryTraceRepository` that stores the last 100 events is used. If you By default, an `InMemoryTraceRepository` that stores the last 100 events is used. If you
need to expand the capacity, you can define your own instance of the need to expand the capacity, you can define your own instance of the
......
...@@ -2660,6 +2660,9 @@ does so, the orders shown in the following table will be used: ...@@ -2660,6 +2660,9 @@ does so, the orders shown in the following table will be used:
|`WebFilterChainProxy` (Spring Security) |`WebFilterChainProxy` (Spring Security)
|`-100` |`-100`
|`HttpTraceWebFilter`
|`Ordered.LOWEST_PRECEDENCE - 10`
|=== |===
...@@ -2794,7 +2797,7 @@ precedence): ...@@ -2794,7 +2797,7 @@ precedence):
|`ErrorPageFilter` |`ErrorPageFilter`
|`Ordered.HIGHEST_PRECEDENCE + 1` |`Ordered.HIGHEST_PRECEDENCE + 1`
|`WebRequestTraceFilter` |`HttpTraceFilter`
|`Ordered.LOWEST_PRECEDENCE - 10` |`Ordered.LOWEST_PRECEDENCE - 10`
|=== |===
......
...@@ -18,6 +18,4 @@ spring.jmx.enabled=true ...@@ -18,6 +18,4 @@ spring.jmx.enabled=true
spring.jackson.serialization.write_dates_as_timestamps=false spring.jackson.serialization.write_dates_as_timestamps=false
management.trace.include=REQUEST_HEADERS,RESPONSE_HEADERS,ERRORS,PATH_INFO,\ management.trace.include=REQUEST_HEADERS,RESPONSE_HEADERS,PRINCIPAL,REMOTE_ADDRESS,SESSION_ID
PATH_TRANSLATED,CONTEXT_PATH,USER_PRINCIPAL,PARAMETERS,QUERY_STRING,AUTH_TYPE,\
REMOTE_ADDRESS,SESSION_ID,REMOTE_USER
...@@ -166,41 +166,6 @@ public class SampleActuatorApplicationTests { ...@@ -166,41 +166,6 @@ public class SampleActuatorApplicationTests {
assertThat(body).contains("This application has no explicit mapping for /error"); assertThat(body).contains("This application has no explicit mapping for /error");
} }
@Test
@SuppressWarnings("unchecked")
public void testTrace() {
this.restTemplate.getForEntity("/health", String.class);
@SuppressWarnings("rawtypes")
ResponseEntity<Map> entity = this.restTemplate
.withBasicAuth("user", getPassword())
.getForEntity("/actuator/trace", Map.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
Map<String, Object> body = entity.getBody();
Map<String, Object> trace = ((List<Map<String, Object>>) body.get("traces"))
.get(0);
Map<String, Object> map = (Map<String, Object>) ((Map<String, Object>) ((Map<String, Object>) trace
.get("info")).get("headers")).get("response");
assertThat(map.get("status")).isEqualTo("200");
}
@Test
@SuppressWarnings("unchecked")
public void traceWithParameterMap() {
this.restTemplate.withBasicAuth("user", getPassword())
.getForEntity("/actuator/health?param1=value1", String.class);
@SuppressWarnings("rawtypes")
ResponseEntity<Map> entity = this.restTemplate
.withBasicAuth("user", getPassword())
.getForEntity("/actuator/trace", Map.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
Map<String, Object> body = entity.getBody();
Map<String, Object> trace = ((List<Map<String, Object>>) body.get("traces"))
.get(0);
Map<String, Object> map = (Map<String, Object>) ((Map<String, Object>) trace
.get("info")).get("parameters");
assertThat(map.get("param1")).isNotNull();
}
@Test @Test
public void testErrorPageDirectAccess() { public void testErrorPageDirectAccess() {
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment