Rework HTTP exchange tracing and add support for WebFlux
Closes gh-9980
This commit is contained in:
parent
11064b5d78
commit
3565961d68
|
|
@ -0,0 +1,29 @@
|
||||||
|
[[trace]]
|
||||||
|
= Trace (`trace`)
|
||||||
|
|
||||||
|
The `trace` endpoint provides information about HTTP request-response exchanges.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[trace-retrieving]]
|
||||||
|
== Retrieving the Traces
|
||||||
|
|
||||||
|
To retrieve the traces, make a `GET` request to `/actuator/trace`, as shown in the
|
||||||
|
following curl-based example:
|
||||||
|
|
||||||
|
include::{snippets}trace/curl-request.adoc[]
|
||||||
|
|
||||||
|
The resulting response is similar to the following:
|
||||||
|
|
||||||
|
include::{snippets}trace/http-response.adoc[]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[trace-retrieving-response-structure]]
|
||||||
|
=== Response Structure
|
||||||
|
|
||||||
|
The response contains details of the traced HTTP request-response exchanges. The
|
||||||
|
following table describes the structure of the response:
|
||||||
|
|
||||||
|
[cols="2,1,3"]
|
||||||
|
include::{snippets}trace/response-fields.adoc[]
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2017 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.boot.actuate.autoconfigure.trace;
|
|
||||||
|
|
||||||
import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
|
|
||||||
import org.springframework.boot.actuate.trace.TraceRepository;
|
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EnableAutoConfiguration Auto-configuration} for {@link TraceRepository tracing}.
|
|
||||||
*
|
|
||||||
* @author Dave Syer
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
public class TraceRepositoryAutoConfiguration {
|
|
||||||
|
|
||||||
@ConditionalOnMissingBean(TraceRepository.class)
|
|
||||||
@Bean
|
|
||||||
public InMemoryTraceRepository traceRepository() {
|
|
||||||
return new InMemoryTraceRepository();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2017 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.boot.actuate.autoconfigure.trace;
|
|
||||||
|
|
||||||
import javax.servlet.Servlet;
|
|
||||||
import javax.servlet.ServletRegistration;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
|
||||||
import org.springframework.boot.actuate.trace.TraceRepository;
|
|
||||||
import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.boot.web.servlet.error.ErrorAttributes;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.web.servlet.DispatcherServlet;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EnableAutoConfiguration Auto-configuration} for {@link WebRequestTraceFilter
|
|
||||||
* tracing}.
|
|
||||||
*
|
|
||||||
* @author Dave Syer
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, ServletRegistration.class })
|
|
||||||
@AutoConfigureAfter(TraceRepositoryAutoConfiguration.class)
|
|
||||||
@ConditionalOnProperty(prefix = "management.trace.filter", name = "enabled", matchIfMissing = true)
|
|
||||||
@EnableConfigurationProperties(TraceEndpointProperties.class)
|
|
||||||
public class TraceWebFilterAutoConfiguration {
|
|
||||||
|
|
||||||
private final TraceRepository traceRepository;
|
|
||||||
|
|
||||||
private final TraceEndpointProperties endpointProperties;
|
|
||||||
|
|
||||||
private final ErrorAttributes errorAttributes;
|
|
||||||
|
|
||||||
public TraceWebFilterAutoConfiguration(TraceRepository traceRepository,
|
|
||||||
TraceEndpointProperties endpointProperties,
|
|
||||||
ObjectProvider<ErrorAttributes> errorAttributes) {
|
|
||||||
this.traceRepository = traceRepository;
|
|
||||||
this.endpointProperties = endpointProperties;
|
|
||||||
this.errorAttributes = errorAttributes.getIfAvailable();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnMissingBean
|
|
||||||
public WebRequestTraceFilter webRequestLoggingFilter() {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.traceRepository,
|
|
||||||
this.endpointProperties.getInclude());
|
|
||||||
if (this.errorAttributes != null) {
|
|
||||||
filter.setErrorAttributes(this.errorAttributes);
|
|
||||||
}
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.autoconfigure.web.trace;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
|
||||||
|
import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository;
|
||||||
|
import org.springframework.boot.actuate.web.trace.reactive.HttpTraceWebFilter;
|
||||||
|
import org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link EnableAutoConfiguration Auto-configuration} for HTTP tracing.
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnWebApplication
|
||||||
|
@ConditionalOnProperty(prefix = "management.trace", name = "enabled", matchIfMissing = true)
|
||||||
|
@EnableConfigurationProperties(TraceProperties.class)
|
||||||
|
public class TraceAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(HttpTraceRepository.class)
|
||||||
|
public InMemoryHttpTraceRepository traceRepository() {
|
||||||
|
return new InMemoryHttpTraceRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public HttpExchangeTracer httpExchangeTracer(TraceProperties traceProperties) {
|
||||||
|
return new HttpExchangeTracer(traceProperties.getInclude());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConditionalOnWebApplication(type = Type.SERVLET)
|
||||||
|
static class ServletTraceFilterConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public HttpTraceFilter httpTraceFilter(HttpTraceRepository repository,
|
||||||
|
HttpExchangeTracer tracer) {
|
||||||
|
return new HttpTraceFilter(repository, tracer);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConditionalOnWebApplication(type = Type.REACTIVE)
|
||||||
|
static class ReactiveTraceFilterConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public HttpTraceWebFilter httpTraceWebFilter(HttpTraceRepository repository,
|
||||||
|
HttpExchangeTracer tracer, TraceProperties traceProperties) {
|
||||||
|
return new HttpTraceWebFilter(repository, tracer,
|
||||||
|
traceProperties.getInclude());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2017 the original author or authors.
|
* Copyright 2012-2018 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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 @@
|
||||||
* 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()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2017 the original author or authors.
|
* Copyright 2012-2018 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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 @@
|
||||||
* 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;
|
||||||
* @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());
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2017 the original author or authors.
|
* Copyright 2012-2018 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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 @@
|
||||||
/**
|
/**
|
||||||
* 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 @@
|
||||||
"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 @@
|
||||||
"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
|
||||||
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,\
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTrace;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTraceEndpoint;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
|
||||||
|
import org.springframework.boot.actuate.web.trace.Include;
|
||||||
|
import org.springframework.boot.actuate.web.trace.TraceableRequest;
|
||||||
|
import org.springframework.boot.actuate.web.trace.TraceableResponse;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.restdocs.payload.JsonFieldType;
|
||||||
|
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||||
|
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||||
|
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for generating documentation describing {@link HttpTraceEndpoint}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public class HttpTraceEndpointDocumentationTests
|
||||||
|
extends MockMvcEndpointDocumentationTests {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private HttpTraceRepository repository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void traces() throws Exception {
|
||||||
|
TraceableRequest request = mock(TraceableRequest.class);
|
||||||
|
given(request.getUri()).willReturn(URI.create("https://api.example.com"));
|
||||||
|
given(request.getMethod()).willReturn("GET");
|
||||||
|
given(request.getHeaders()).willReturn(Collections
|
||||||
|
.singletonMap(HttpHeaders.ACCEPT, Arrays.asList("application/json")));
|
||||||
|
TraceableResponse response = mock(TraceableResponse.class);
|
||||||
|
given(response.getStatus()).willReturn(200);
|
||||||
|
given(response.getHeaders()).willReturn(Collections.singletonMap(
|
||||||
|
HttpHeaders.CONTENT_TYPE, Arrays.asList("application/json")));
|
||||||
|
Principal principal = mock(Principal.class);
|
||||||
|
given(principal.getName()).willReturn("alice");
|
||||||
|
HttpExchangeTracer tracer = new HttpExchangeTracer(EnumSet.allOf(Include.class));
|
||||||
|
HttpTrace trace = tracer.receivedRequest(request);
|
||||||
|
tracer.sendingResponse(trace, response, () -> principal,
|
||||||
|
() -> UUID.randomUUID().toString());
|
||||||
|
given(this.repository.findAll()).willReturn(Arrays.asList(trace));
|
||||||
|
this.mockMvc.perform(get("/actuator/trace")).andExpect(status().isOk())
|
||||||
|
.andDo(document("trace",
|
||||||
|
responseFields(
|
||||||
|
fieldWithPath("traces").description(
|
||||||
|
"An array of traced HTTP request-response exchanges."),
|
||||||
|
fieldWithPath("traces.[].timestamp").description(
|
||||||
|
"Timestamp of when the traced exchange occurred."),
|
||||||
|
fieldWithPath("traces.[].principal")
|
||||||
|
.description("Principal of the exchange, if any.")
|
||||||
|
.optional(),
|
||||||
|
fieldWithPath("traces.[].principal.name")
|
||||||
|
.description("Name of the principal.").optional(),
|
||||||
|
fieldWithPath("traces.[].request.method")
|
||||||
|
.description("HTTP method of the request."),
|
||||||
|
fieldWithPath("traces.[].request.remoteAddress")
|
||||||
|
.description(
|
||||||
|
"Remote address from which the request was received, if known.")
|
||||||
|
.optional().type(JsonFieldType.STRING),
|
||||||
|
fieldWithPath("traces.[].request.uri")
|
||||||
|
.description("URI of the request."),
|
||||||
|
fieldWithPath("traces.[].request.headers").description(
|
||||||
|
"Headers of the request, keyed by header name."),
|
||||||
|
fieldWithPath("traces.[].request.headers.*.[]")
|
||||||
|
.description("Values of the header"),
|
||||||
|
fieldWithPath("traces.[].response.status")
|
||||||
|
.description("Status of the response"),
|
||||||
|
fieldWithPath("traces.[].response.headers").description(
|
||||||
|
"Headers of the response, keyed by header name."),
|
||||||
|
fieldWithPath("traces.[].response.headers.*.[]")
|
||||||
|
.description("Values of the header"),
|
||||||
|
fieldWithPath("traces.[].session")
|
||||||
|
.description(
|
||||||
|
"Session associated with the exchange, if any.")
|
||||||
|
.optional(),
|
||||||
|
fieldWithPath("traces.[].session.id")
|
||||||
|
.description("ID of the session."),
|
||||||
|
fieldWithPath("traces.[].timeTaken").description(
|
||||||
|
"Time, in milliseconds, taken to handle the exchange."))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Import(BaseDocumentationConfiguration.class)
|
||||||
|
static class TestConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public HttpTraceEndpoint httpTraceEndpoint(HttpTraceRepository repository) {
|
||||||
|
return new HttpTraceEndpoint(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2017 the original author or authors.
|
* Copyright 2012-2018 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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;
|
||||||
|
|
||||||
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;
|
||||||
*/
|
*/
|
||||||
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
|
||||||
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 {
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2017 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.boot.actuate.autoconfigure.trace;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
|
|
||||||
import org.springframework.boot.actuate.trace.TraceRepository;
|
|
||||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests for {@link TraceRepositoryAutoConfiguration}.
|
|
||||||
*
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
public class TraceRepositoryAutoConfigurationTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void configuresInMemoryTraceRepository() {
|
|
||||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
|
||||||
TraceRepositoryAutoConfiguration.class);
|
|
||||||
assertThat(context.getBean(InMemoryTraceRepository.class)).isNotNull();
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void skipsIfRepositoryExists() {
|
|
||||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
|
||||||
Config.class, TraceRepositoryAutoConfiguration.class);
|
|
||||||
assertThat(context.getBeansOfType(InMemoryTraceRepository.class)).isEmpty();
|
|
||||||
assertThat(context.getBeansOfType(TraceRepository.class)).hasSize(1);
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public static class Config {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public TraceRepository traceRepository() {
|
|
||||||
return mock(TraceRepository.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2017 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.boot.actuate.autoconfigure.trace;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import org.springframework.boot.actuate.trace.TraceRepository;
|
|
||||||
import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
|
|
||||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
|
||||||
import org.springframework.boot.test.util.TestPropertyValues;
|
|
||||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests for {@link TraceWebFilterAutoConfiguration}.
|
|
||||||
*
|
|
||||||
* @author Phillip Webb
|
|
||||||
* @author Stephane Nicoll
|
|
||||||
*/
|
|
||||||
public class TraceWebFilterAutoConfigurationTests {
|
|
||||||
|
|
||||||
private AnnotationConfigApplicationContext context;
|
|
||||||
|
|
||||||
@After
|
|
||||||
public void close() {
|
|
||||||
if (this.context != null) {
|
|
||||||
this.context.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void configureFilter() {
|
|
||||||
load();
|
|
||||||
assertThat(this.context.getBean(WebRequestTraceFilter.class)).isNotNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void overrideTraceFilter() {
|
|
||||||
load(CustomTraceFilterConfig.class);
|
|
||||||
WebRequestTraceFilter filter = this.context.getBean(WebRequestTraceFilter.class);
|
|
||||||
assertThat(filter).isInstanceOf(TestWebRequestTraceFilter.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void skipsFilterIfPropertyDisabled() {
|
|
||||||
load("management.trace.filter.enabled:false");
|
|
||||||
assertThat(this.context.getBeansOfType(WebRequestTraceFilter.class).size())
|
|
||||||
.isEqualTo(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void load(String... environment) {
|
|
||||||
load(null, environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void load(Class<?> config, String... environment) {
|
|
||||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
|
||||||
TestPropertyValues.of(environment).applyTo(context);
|
|
||||||
if (config != null) {
|
|
||||||
context.register(config);
|
|
||||||
}
|
|
||||||
context.register(PropertyPlaceholderAutoConfiguration.class,
|
|
||||||
TraceRepositoryAutoConfiguration.class,
|
|
||||||
TraceWebFilterAutoConfiguration.class);
|
|
||||||
context.refresh();
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
static class CustomTraceFilterConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public TestWebRequestTraceFilter testWebRequestTraceFilter(
|
|
||||||
TraceRepository repository, TraceEndpointProperties properties) {
|
|
||||||
return new TestWebRequestTraceFilter(repository, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static class TestWebRequestTraceFilter extends WebRequestTraceFilter {
|
|
||||||
|
|
||||||
TestWebRequestTraceFilter(TraceRepository repository,
|
|
||||||
TraceEndpointProperties properties) {
|
|
||||||
super(repository, properties.getInclude());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void postProcessRequestHeaders(Map<String, Object> headers) {
|
|
||||||
headers.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.autoconfigure.web.trace;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTrace;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
|
||||||
|
import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository;
|
||||||
|
import org.springframework.boot.actuate.web.trace.Include;
|
||||||
|
import org.springframework.boot.actuate.web.trace.reactive.HttpTraceWebFilter;
|
||||||
|
import org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||||
|
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link TraceAutoConfiguration}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public class TraceAutoConfigurationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void configuresRepository() {
|
||||||
|
new WebApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
|
||||||
|
.run((context) -> assertThat(context)
|
||||||
|
.hasSingleBean(InMemoryHttpTraceRepository.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void usesUserProvidedRepository() {
|
||||||
|
new WebApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
|
||||||
|
.withUserConfiguration(CustomRepositoryConfiguration.class)
|
||||||
|
.run((context) -> {
|
||||||
|
assertThat(context).hasSingleBean(HttpTraceRepository.class);
|
||||||
|
assertThat(context.getBean(HttpTraceRepository.class))
|
||||||
|
.isInstanceOf(CustomHttpTraceRepository.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void configuresTracer() {
|
||||||
|
new WebApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
|
||||||
|
.run((context) -> assertThat(context)
|
||||||
|
.hasSingleBean(HttpExchangeTracer.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void usesUserProvidedTracer() {
|
||||||
|
new WebApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
|
||||||
|
.withUserConfiguration(CustomTracerConfiguration.class).run((context) -> {
|
||||||
|
assertThat(context).hasSingleBean(HttpExchangeTracer.class);
|
||||||
|
assertThat(context.getBean(HttpExchangeTracer.class))
|
||||||
|
.isInstanceOf(CustomHttpExchangeTracer.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void configuresWebFilter() {
|
||||||
|
new ReactiveWebApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
|
||||||
|
.run((context) -> assertThat(context)
|
||||||
|
.hasSingleBean(HttpTraceWebFilter.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void usesUserProvidedWebFilter() {
|
||||||
|
new ReactiveWebApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
|
||||||
|
.withUserConfiguration(CustomWebFilterConfiguration.class)
|
||||||
|
.run((context) -> {
|
||||||
|
assertThat(context).hasSingleBean(HttpTraceWebFilter.class);
|
||||||
|
assertThat(context.getBean(HttpTraceWebFilter.class))
|
||||||
|
.isInstanceOf(CustomHttpTraceWebFilter.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void configuresServletFilter() {
|
||||||
|
new WebApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
|
||||||
|
.run((context) -> assertThat(context)
|
||||||
|
.hasSingleBean(HttpTraceFilter.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void usesUserProvidedServletFilter() {
|
||||||
|
new WebApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
|
||||||
|
.withUserConfiguration(CustomFilterConfiguration.class).run((context) -> {
|
||||||
|
assertThat(context).hasSingleBean(HttpTraceFilter.class);
|
||||||
|
assertThat(context.getBean(HttpTraceFilter.class))
|
||||||
|
.isInstanceOf(CustomHttpTraceFilter.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void backsOffWhenDisabled() {
|
||||||
|
new WebApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(TraceAutoConfiguration.class))
|
||||||
|
.withPropertyValues("management.trace.enabled=false")
|
||||||
|
.run((context) -> assertThat(context)
|
||||||
|
.doesNotHaveBean(InMemoryHttpTraceRepository.class)
|
||||||
|
.doesNotHaveBean(HttpExchangeTracer.class)
|
||||||
|
.doesNotHaveBean(HttpTraceFilter.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CustomHttpTraceRepository implements HttpTraceRepository {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2017 the original author or authors.
|
* Copyright 2012-2018 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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 @@
|
||||||
* 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;
|
||||||
*/
|
*/
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2018 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.boot.actuate.trace;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A value object representing a trace event: at a particular time with a simple (map)
|
|
||||||
* information. Can be used for analyzing contextual information such as HTTP headers.
|
|
||||||
*
|
|
||||||
* @author Dave Syer
|
|
||||||
*/
|
|
||||||
public final class Trace {
|
|
||||||
|
|
||||||
private final Instant timestamp;
|
|
||||||
|
|
||||||
private final Map<String, Object> info;
|
|
||||||
|
|
||||||
public Trace(Instant timestamp, Map<String, Object> info) {
|
|
||||||
Assert.notNull(timestamp, "Timestamp must not be null");
|
|
||||||
Assert.notNull(info, "Info must not be null");
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
this.info = info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Instant getTimestamp() {
|
|
||||||
return this.timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Object> getInfo() {
|
|
||||||
return this.info;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,283 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2017 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.boot.actuate.trace;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.Principal;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import javax.servlet.Filter;
|
|
||||||
import javax.servlet.FilterChain;
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import javax.servlet.http.HttpServletResponseWrapper;
|
|
||||||
import javax.servlet.http.HttpSession;
|
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
|
||||||
import org.apache.commons.logging.LogFactory;
|
|
||||||
|
|
||||||
import org.springframework.boot.web.servlet.error.ErrorAttributes;
|
|
||||||
import org.springframework.core.Ordered;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.context.request.ServletWebRequest;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Servlet {@link Filter} that logs all requests to a {@link TraceRepository}.
|
|
||||||
*
|
|
||||||
* @author Dave Syer
|
|
||||||
* @author Wallace Wadge
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
* @author Venil Noronha
|
|
||||||
* @author Madhura Bhave
|
|
||||||
*/
|
|
||||||
public class WebRequestTraceFilter extends OncePerRequestFilter implements Ordered {
|
|
||||||
|
|
||||||
private static final Log logger = LogFactory.getLog(WebRequestTraceFilter.class);
|
|
||||||
|
|
||||||
private boolean dumpRequests = false;
|
|
||||||
|
|
||||||
// Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all
|
|
||||||
// enriched headers, but users can add stuff after this if they want to
|
|
||||||
private int order = Ordered.LOWEST_PRECEDENCE - 10;
|
|
||||||
|
|
||||||
private final TraceRepository repository;
|
|
||||||
|
|
||||||
private ErrorAttributes errorAttributes;
|
|
||||||
|
|
||||||
private final Set<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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traces an HTTP request-response exchange.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class HttpExchangeTracer {
|
||||||
|
|
||||||
|
private final Set<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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A trace event for handling of an HTTP request and response exchange. Can be used for
|
||||||
|
* analyzing contextual information such as HTTP headers.
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public final class HttpTrace {
|
||||||
|
|
||||||
|
private final Instant timestamp = Instant.now();
|
||||||
|
|
||||||
|
private volatile Principal principal;
|
||||||
|
|
||||||
|
private volatile Session session;
|
||||||
|
|
||||||
|
private final Request request;
|
||||||
|
|
||||||
|
private volatile Response response;
|
||||||
|
|
||||||
|
private volatile Long timeTaken;
|
||||||
|
|
||||||
|
HttpTrace(TraceableRequest request) {
|
||||||
|
this.request = new Request(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getTimestamp() {
|
||||||
|
return this.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPrincipal(java.security.Principal principal) {
|
||||||
|
if (principal != null) {
|
||||||
|
this.principal = new Principal(principal.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Principal getPrincipal() {
|
||||||
|
return this.principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Session getSession() {
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSessionId(String sessionId) {
|
||||||
|
if (StringUtils.hasText(sessionId)) {
|
||||||
|
this.session = new Session(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Request getRequest() {
|
||||||
|
return this.request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response getResponse() {
|
||||||
|
return this.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setResponse(Response response) {
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTimeTaken() {
|
||||||
|
return this.timeTaken;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTimeTaken(long timeTaken) {
|
||||||
|
this.timeTaken = timeTaken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trace of an HTTP request.
|
||||||
|
*/
|
||||||
|
public static final class Request {
|
||||||
|
|
||||||
|
private final String method;
|
||||||
|
|
||||||
|
private final URI uri;
|
||||||
|
|
||||||
|
private final Map<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2017 the original author or authors.
|
* Copyright 2012-2018 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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 @@
|
||||||
* 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;
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2017 the original author or authors.
|
* Copyright 2012-2018 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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 @@
|
||||||
* 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 @@
|
||||||
* 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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2017 the original author or authors.
|
* Copyright 2012-2018 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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 @@
|
||||||
* 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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of an HTTP request that is suitable for tracing.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 2.0.0
|
||||||
|
* @see HttpExchangeTracer
|
||||||
|
*/
|
||||||
|
public interface TraceableRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the method (GET, POST, etc) of the request.
|
||||||
|
* @return the method
|
||||||
|
*/
|
||||||
|
String getMethod();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URI of the request.
|
||||||
|
* @return the URI
|
||||||
|
*/
|
||||||
|
URI getUri();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a modifiable copy of the headers of the request.
|
||||||
|
* @return the headers
|
||||||
|
*/
|
||||||
|
Map<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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2017 the original author or authors.
|
* Copyright 2012-2018 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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 @@
|
||||||
* 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 {
|
||||||
|
|
||||||
@Test
|
/**
|
||||||
public void trace() {
|
* The status of the response.
|
||||||
TraceRepository repository = new InMemoryTraceRepository();
|
* @return the status
|
||||||
repository.add(Collections.singletonMap("a", "b"));
|
*/
|
||||||
Trace trace = new TraceEndpoint(repository).traces().getTraces().get(0);
|
int getStatus();
|
||||||
assertThat(trace.getInfo().get("a")).isEqualTo("b");
|
|
||||||
}
|
/**
|
||||||
|
* Returns a modifiable copy of the headers of the response.
|
||||||
|
* @return the headers
|
||||||
|
*/
|
||||||
|
Map<String, List<String>> getHeaders();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2017 the original author or authors.
|
* Copyright 2012-2018 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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;
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace.reactive;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTrace;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
|
||||||
|
import org.springframework.boot.actuate.web.trace.Include;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import org.springframework.web.server.WebFilter;
|
||||||
|
import org.springframework.web.server.WebFilterChain;
|
||||||
|
import org.springframework.web.server.WebSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link WebFilter} for tracing HTTP requests.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class HttpTraceWebFilter implements WebFilter, Ordered {
|
||||||
|
|
||||||
|
private static final Object NONE = new Object();
|
||||||
|
|
||||||
|
// Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all
|
||||||
|
// enriched headers, but users can add stuff after this if they want to
|
||||||
|
private int order = Ordered.LOWEST_PRECEDENCE - 10;
|
||||||
|
|
||||||
|
private final HttpTraceRepository repository;
|
||||||
|
|
||||||
|
private final HttpExchangeTracer tracer;
|
||||||
|
|
||||||
|
private final Set<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace.reactive;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.TraceableRequest;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link TraceableRequest} backed by a {@link ServerWebExchange}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
class ServerWebExchangeTraceableRequest implements TraceableRequest {
|
||||||
|
|
||||||
|
private final String method;
|
||||||
|
|
||||||
|
private final Map<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace.reactive;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.TraceableResponse;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An adapter that exposes a {@link ServerHttpResponse} as a {@link TraceableResponse}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
class TraceableServerHttpResponse implements TraceableResponse {
|
||||||
|
|
||||||
|
private final ServerHttpResponse response;
|
||||||
|
|
||||||
|
TraceableServerHttpResponse(ServerHttpResponse exchange) {
|
||||||
|
this.response = exchange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getStatus() {
|
||||||
|
return this.response.getStatusCode() == null ? 200
|
||||||
|
: this.response.getStatusCode().value();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<String>> getHeaders() {
|
||||||
|
return new LinkedHashMap<>(this.response.getHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actuator reactive HTTP tracing support.
|
||||||
|
*
|
||||||
|
* @see org.springframework.boot.actuate.web.trace.HttpTraceRepository
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.actuate.web.trace.reactive;
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace.servlet;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import javax.servlet.Filter;
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.servlet.http.HttpServletResponseWrapper;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTrace;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Servlet {@link Filter} that logs all requests to an {@link HttpTraceRepository}.
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Wallace Wadge
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Venil Noronha
|
||||||
|
* @author Madhura Bhave
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class HttpTraceFilter extends OncePerRequestFilter implements Ordered {
|
||||||
|
|
||||||
|
// Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all
|
||||||
|
// enriched headers, but users can add stuff after this if they want to
|
||||||
|
private int order = Ordered.LOWEST_PRECEDENCE - 10;
|
||||||
|
|
||||||
|
private final HttpTraceRepository repository;
|
||||||
|
|
||||||
|
private final HttpExchangeTracer tracer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link HttpTraceFilter} instance.
|
||||||
|
* @param repository the trace repository
|
||||||
|
* @param tracer used to trace exchanges
|
||||||
|
*/
|
||||||
|
public HttpTraceFilter(HttpTraceRepository repository, HttpExchangeTracer tracer) {
|
||||||
|
this.repository = repository;
|
||||||
|
this.tracer = tracer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return this.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrder(int order) {
|
||||||
|
this.order = order;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
|
HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
TraceableHttpServletRequest traceableRequest = new TraceableHttpServletRequest(
|
||||||
|
request);
|
||||||
|
HttpTrace trace = this.tracer.receivedRequest(traceableRequest);
|
||||||
|
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
|
||||||
|
try {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
status = response.getStatus();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
TraceableHttpServletResponse traceableResponse = new TraceableHttpServletResponse(
|
||||||
|
status == response.getStatus() ? response
|
||||||
|
: new CustomStatusResponseWrapper(response, status));
|
||||||
|
this.tracer.sendingResponse(trace, traceableResponse,
|
||||||
|
request::getUserPrincipal, () -> getSessionId(request));
|
||||||
|
this.repository.add(trace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSessionId(HttpServletRequest request) {
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
return session == null ? null : session.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CustomStatusResponseWrapper
|
||||||
|
extends HttpServletResponseWrapper {
|
||||||
|
|
||||||
|
private final int status;
|
||||||
|
|
||||||
|
private CustomStatusResponseWrapper(HttpServletResponse response, int status) {
|
||||||
|
super(response);
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getStatus() {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace.servlet;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.TraceableRequest;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An adapter that exposes an {@link HttpServletRequest} as a {@link TraceableRequest}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
final class TraceableHttpServletRequest implements TraceableRequest {
|
||||||
|
|
||||||
|
private final HttpServletRequest request;
|
||||||
|
|
||||||
|
TraceableHttpServletRequest(HttpServletRequest request) {
|
||||||
|
this.request = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMethod() {
|
||||||
|
return this.request.getMethod();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URI getUri() {
|
||||||
|
StringBuffer urlBuffer = this.request.getRequestURL();
|
||||||
|
if (StringUtils.hasText(this.request.getQueryString())) {
|
||||||
|
urlBuffer.append("?");
|
||||||
|
urlBuffer.append(this.request.getQueryString());
|
||||||
|
}
|
||||||
|
return URI.create(urlBuffer.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace.servlet;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.TraceableResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An adapter that exposes an {@link HttpServletResponse} as a {@link TraceableResponse}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
final class TraceableHttpServletResponse implements TraceableResponse {
|
||||||
|
|
||||||
|
private final HttpServletResponse delegate;
|
||||||
|
|
||||||
|
TraceableHttpServletResponse(HttpServletResponse response) {
|
||||||
|
this.delegate = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getStatus() {
|
||||||
|
return this.delegate.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actuator servlet HTTP tracing support.
|
||||||
|
*
|
||||||
|
* @see org.springframework.boot.actuate.web.trace.HttpTraceRepository
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.actuate.web.trace.servlet;
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2017 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.boot.actuate.trace;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests for {@link InMemoryTraceRepository}.
|
|
||||||
*
|
|
||||||
* @author Dave Syer
|
|
||||||
*/
|
|
||||||
public class InMemoryTraceRepositoryTests {
|
|
||||||
|
|
||||||
private final InMemoryTraceRepository repository = new InMemoryTraceRepository();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void capacityLimited() {
|
|
||||||
this.repository.setCapacity(2);
|
|
||||||
this.repository.add(Collections.singletonMap("foo", "bar"));
|
|
||||||
this.repository.add(Collections.singletonMap("bar", "foo"));
|
|
||||||
this.repository.add(Collections.singletonMap("bar", "bar"));
|
|
||||||
List<Trace> traces = this.repository.findAll();
|
|
||||||
assertThat(traces).hasSize(2);
|
|
||||||
assertThat(traces.get(0).getInfo().get("bar")).isEqualTo("bar");
|
|
||||||
assertThat(traces.get(1).getInfo().get("bar")).isEqualTo("foo");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void reverseFalse() {
|
|
||||||
this.repository.setReverse(false);
|
|
||||||
this.repository.setCapacity(2);
|
|
||||||
this.repository.add(Collections.singletonMap("foo", "bar"));
|
|
||||||
this.repository.add(Collections.singletonMap("bar", "foo"));
|
|
||||||
this.repository.add(Collections.singletonMap("bar", "bar"));
|
|
||||||
List<Trace> traces = this.repository.findAll();
|
|
||||||
assertThat(traces).hasSize(2);
|
|
||||||
assertThat(traces.get(1).getInfo().get("bar")).isEqualTo("bar");
|
|
||||||
assertThat(traces.get(0).getInfo().get("bar")).isEqualTo("foo");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,285 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2017 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.boot.actuate.trace;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.Principal;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
|
|
||||||
import org.springframework.mock.web.MockFilterChain;
|
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
|
||||||
import org.springframework.mock.web.MockHttpServletResponse;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.junit.Assert.fail;
|
|
||||||
import static org.mockito.Mockito.spy;
|
|
||||||
import static org.mockito.Mockito.times;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests for {@link WebRequestTraceFilter}.
|
|
||||||
*
|
|
||||||
* @author Dave Syer
|
|
||||||
* @author Wallace Wadge
|
|
||||||
* @author Phillip Webb
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
* @author Venil Noronha
|
|
||||||
* @author Stephane Nicoll
|
|
||||||
* @author Madhura Bhave
|
|
||||||
*/
|
|
||||||
public class WebRequestTraceFilterTests {
|
|
||||||
|
|
||||||
private final InMemoryTraceRepository repository = new InMemoryTraceRepository();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void filterAddsTraceWithDefaultIncludes() {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository);
|
|
||||||
MockHttpServletRequest request = spy(new MockHttpServletRequest("GET", "/foo"));
|
|
||||||
request.addHeader("Accept", "application/json");
|
|
||||||
Map<String, Object> trace = filter.getTrace(request);
|
|
||||||
assertThat(trace.get("method")).isEqualTo("GET");
|
|
||||||
assertThat(trace.get("path")).isEqualTo("/foo");
|
|
||||||
Map<String, Object> map = (Map<String, Object>) trace.get("headers");
|
|
||||||
assertThat(map.get("request").toString()).isEqualTo("{Accept=application/json}");
|
|
||||||
verify(request, times(0)).getParameterMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
|
||||||
public void filterAddsTraceWithCustomIncludes() throws IOException, ServletException {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository,
|
|
||||||
EnumSet.allOf(Include.class));
|
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
|
||||||
request.addHeader("Accept", "application/json");
|
|
||||||
request.addHeader("Cookie", "testCookie=testValue;");
|
|
||||||
request.setContextPath("some.context.path");
|
|
||||||
request.setContent("Hello, World!".getBytes());
|
|
||||||
request.setRemoteAddr("some.remote.addr");
|
|
||||||
request.setQueryString("some.query.string");
|
|
||||||
request.setParameter("param", "paramvalue");
|
|
||||||
File tmp = File.createTempFile("spring-boot", "tmp");
|
|
||||||
String url = tmp.toURI().toURL().toString();
|
|
||||||
request.setPathInfo(url);
|
|
||||||
tmp.deleteOnExit();
|
|
||||||
request.setAuthType("authType");
|
|
||||||
Principal principal = () -> "principalTest";
|
|
||||||
request.setUserPrincipal(principal);
|
|
||||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
|
||||||
response.addHeader("Content-Type", "application/json");
|
|
||||||
response.addHeader("Set-Cookie", "a=b");
|
|
||||||
filter.doFilterInternal(request, response, (req, resp) -> {
|
|
||||||
BufferedReader bufferedReader = req.getReader();
|
|
||||||
while (bufferedReader.readLine() != null) {
|
|
||||||
// read the contents as normal (forces cache to fill up)
|
|
||||||
}
|
|
||||||
resp.getWriter().println("Goodbye, World!");
|
|
||||||
});
|
|
||||||
assertThat(this.repository.findAll()).hasSize(1);
|
|
||||||
Map<String, Object> trace = this.repository.findAll().iterator().next().getInfo();
|
|
||||||
Map<String, Object> map = (Map<String, Object>) trace.get("headers");
|
|
||||||
|
|
||||||
assertThat(map.get("response").toString())
|
|
||||||
.isEqualTo("{Content-Type=application/json, Set-Cookie=a=b, status=200}");
|
|
||||||
assertThat(trace.get("method")).isEqualTo("GET");
|
|
||||||
assertThat(trace.get("path")).isEqualTo("/foo");
|
|
||||||
assertThat(((String[]) ((Map) trace.get("parameters")).get("param"))[0])
|
|
||||||
.isEqualTo("paramvalue");
|
|
||||||
assertThat(trace.get("remoteAddress")).isEqualTo("some.remote.addr");
|
|
||||||
assertThat(trace.get("query")).isEqualTo("some.query.string");
|
|
||||||
assertThat(trace.get("userPrincipal")).isEqualTo(principal.getName());
|
|
||||||
assertThat(trace.get("contextPath")).isEqualTo("some.context.path");
|
|
||||||
assertThat(trace.get("pathInfo")).isEqualTo(url);
|
|
||||||
assertThat(trace.get("authType")).isEqualTo("authType");
|
|
||||||
assertThat(map.get("request").toString())
|
|
||||||
.isEqualTo("{Accept=application/json, Cookie=testCookie=testValue;}");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings({ "unchecked" })
|
|
||||||
public void filterDoesNotAddResponseHeadersWithoutResponseHeadersInclude()
|
|
||||||
throws ServletException, IOException {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository,
|
|
||||||
Collections.singleton(Include.REQUEST_HEADERS));
|
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
|
||||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
|
||||||
response.addHeader("Content-Type", "application/json");
|
|
||||||
filter.doFilterInternal(request, response, (req, resp) -> {
|
|
||||||
});
|
|
||||||
Map<String, Object> info = this.repository.findAll().iterator().next().getInfo();
|
|
||||||
Map<String, Object> headers = (Map<String, Object>) info.get("headers");
|
|
||||||
assertThat(headers.get("response") == null).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings({ "unchecked" })
|
|
||||||
public void filterDoesNotAddRequestCookiesWithCookiesExclude() {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository,
|
|
||||||
Collections.singleton(Include.REQUEST_HEADERS));
|
|
||||||
MockHttpServletRequest request = spy(new MockHttpServletRequest("GET", "/foo"));
|
|
||||||
request.addHeader("Accept", "application/json");
|
|
||||||
request.addHeader("Cookie", "testCookie=testValue;");
|
|
||||||
Map<String, Object> map = (Map<String, Object>) filter.getTrace(request)
|
|
||||||
.get("headers");
|
|
||||||
assertThat(map.get("request").toString()).isEqualTo("{Accept=application/json}");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings({ "unchecked" })
|
|
||||||
public void filterDoesNotAddAuthorizationHeaderWithoutAuthorizationHeaderInclude()
|
|
||||||
throws ServletException, IOException {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository);
|
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
|
||||||
request.addHeader("Authorization", "my-auth-header");
|
|
||||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
|
||||||
filter.doFilterInternal(request, response, (req, resp) -> {
|
|
||||||
});
|
|
||||||
Map<String, Object> info = this.repository.findAll().iterator().next().getInfo();
|
|
||||||
Map<String, Object> headers = (Map<String, Object>) info.get("headers");
|
|
||||||
assertThat(((Map<Object, Object>) headers.get("request"))).hasSize(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings({ "unchecked" })
|
|
||||||
public void filterAddsAuthorizationHeaderWhenAuthorizationHeaderIncluded()
|
|
||||||
throws ServletException, IOException {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository,
|
|
||||||
EnumSet.of(Include.REQUEST_HEADERS, Include.AUTHORIZATION_HEADER));
|
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
|
||||||
request.addHeader("Authorization", "my-auth-header");
|
|
||||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
|
||||||
filter.doFilterInternal(request, response, (req, resp) -> {
|
|
||||||
});
|
|
||||||
Map<String, Object> info = this.repository.findAll().iterator().next().getInfo();
|
|
||||||
Map<String, Object> headers = (Map<String, Object>) info.get("headers");
|
|
||||||
assertThat(((Map<Object, Object>) headers.get("request")))
|
|
||||||
.containsKey("Authorization");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings({ "unchecked" })
|
|
||||||
public void filterDoesNotAddResponseCookiesWithCookiesExclude() {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository,
|
|
||||||
Collections.singleton(Include.RESPONSE_HEADERS));
|
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
|
||||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
|
||||||
response.addHeader("Content-Type", "application/json");
|
|
||||||
response.addHeader("Set-Cookie", "testCookie=testValue;");
|
|
||||||
Map<String, Object> trace = filter.getTrace(request);
|
|
||||||
filter.enhanceTrace(trace, response);
|
|
||||||
Map<String, Object> map = (Map<String, Object>) trace.get("headers");
|
|
||||||
assertThat(map.get("response").toString())
|
|
||||||
.isEqualTo("{Content-Type=application/json, status=200}");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void filterHasResponseStatus() {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository);
|
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
|
||||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
|
||||||
response.setStatus(404);
|
|
||||||
response.addHeader("Content-Type", "application/json");
|
|
||||||
Map<String, Object> trace = filter.getTrace(request);
|
|
||||||
filter.enhanceTrace(trace, response);
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> map = (Map<String, Object>) ((Map<String, Object>) trace
|
|
||||||
.get("headers")).get("response");
|
|
||||||
assertThat(map.get("status").toString()).isEqualTo("404");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void filterAddsTimeTaken() throws Exception {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository);
|
|
||||||
MockHttpServletRequest request = spy(new MockHttpServletRequest("GET", "/foo"));
|
|
||||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
|
||||||
MockFilterChain chain = new MockFilterChain();
|
|
||||||
filter.doFilter(request, response, chain);
|
|
||||||
String timeTaken = (String) this.repository.findAll().iterator().next().getInfo()
|
|
||||||
.get("timeTaken");
|
|
||||||
assertThat(timeTaken).isNotNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void filterHasError() {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository);
|
|
||||||
filter.setErrorAttributes(new DefaultErrorAttributes());
|
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
|
||||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
|
||||||
response.setStatus(500);
|
|
||||||
request.setAttribute("javax.servlet.error.exception",
|
|
||||||
new IllegalStateException("Foo"));
|
|
||||||
response.addHeader("Content-Type", "application/json");
|
|
||||||
Map<String, Object> trace = filter.getTrace(request);
|
|
||||||
filter.enhanceTrace(trace, response);
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> map = (Map<String, Object>) trace.get("error");
|
|
||||||
System.err.println(map);
|
|
||||||
assertThat(map.get("message").toString()).isEqualTo("Foo");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void filterHas500ResponseStatusWhenExceptionIsThrown()
|
|
||||||
throws ServletException, IOException {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository);
|
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
|
||||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
|
||||||
|
|
||||||
try {
|
|
||||||
filter.doFilterInternal(request, response, (req, resp) -> {
|
|
||||||
throw new RuntimeException();
|
|
||||||
});
|
|
||||||
fail("Exception was swallowed");
|
|
||||||
}
|
|
||||||
catch (RuntimeException ex) {
|
|
||||||
Map<String, Object> headers = (Map<String, Object>) this.repository.findAll()
|
|
||||||
.iterator().next().getInfo().get("headers");
|
|
||||||
Map<String, Object> responseHeaders = (Map<String, Object>) headers
|
|
||||||
.get("response");
|
|
||||||
assertThat((String) responseHeaders.get("status")).isEqualTo("500");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void postProcessRequestHeaders() {
|
|
||||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository) {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void postProcessRequestHeaders(Map<String, Object> headers) {
|
|
||||||
headers.remove("Test");
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
MockHttpServletRequest request = spy(new MockHttpServletRequest("GET", "/foo"));
|
|
||||||
request.addHeader("Accept", "application/json");
|
|
||||||
request.addHeader("Test", "spring");
|
|
||||||
Map<String, Object> map = (Map<String, Object>) filter.getTrace(request)
|
|
||||||
.get("headers");
|
|
||||||
assertThat(map.get("request").toString()).isEqualTo("{Accept=application/json}");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,335 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTrace.Request;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link HttpExchangeTracer}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public class HttpExchangeTracerTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void methodIsIncluded() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(EnumSet.noneOf(Include.class))
|
||||||
|
.receivedRequest(createRequest());
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getMethod()).isEqualTo("GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void uriIsIncluded() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(EnumSet.noneOf(Include.class))
|
||||||
|
.receivedRequest(createRequest());
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getUri()).isEqualTo(URI.create("https://api.example.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void remoteAddressIsNotIncludedByDefault() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(EnumSet.noneOf(Include.class))
|
||||||
|
.receivedRequest(createRequest());
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getRemoteAddress()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void remoteAddressCanBeIncluded() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REMOTE_ADDRESS))
|
||||||
|
.receivedRequest(createRequest());
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getRemoteAddress()).isEqualTo("127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestHeadersAreNotIncludedByDefault() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(EnumSet.noneOf(Include.class))
|
||||||
|
.receivedRequest(createRequest());
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getHeaders()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestHeadersCanBeIncluded() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REQUEST_HEADERS))
|
||||||
|
.receivedRequest(createRequest());
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getHeaders()).containsOnlyKeys(HttpHeaders.ACCEPT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizationHeaderIsNotIncludedByDefault() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REQUEST_HEADERS))
|
||||||
|
.receivedRequest(createRequest(Collections.singletonMap(
|
||||||
|
HttpHeaders.AUTHORIZATION, Arrays.asList("secret"))));
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getHeaders()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mixedCaseAuthorizationHeaderIsNotIncludedByDefault() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REQUEST_HEADERS))
|
||||||
|
.receivedRequest(createRequest(Collections.singletonMap(
|
||||||
|
mixedCase(HttpHeaders.AUTHORIZATION), Arrays.asList("secret"))));
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getHeaders()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizationHeaderCanBeIncluded() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(
|
||||||
|
EnumSet.of(Include.REQUEST_HEADERS, Include.AUTHORIZATION_HEADER))
|
||||||
|
.receivedRequest(createRequest(Collections.singletonMap(
|
||||||
|
HttpHeaders.AUTHORIZATION, Arrays.asList("secret"))));
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getHeaders()).containsOnlyKeys(HttpHeaders.AUTHORIZATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mixedCaseAuthorizationHeaderCanBeIncluded() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(
|
||||||
|
EnumSet.of(Include.REQUEST_HEADERS, Include.AUTHORIZATION_HEADER))
|
||||||
|
.receivedRequest(createRequest(Collections.singletonMap(
|
||||||
|
mixedCase(HttpHeaders.AUTHORIZATION),
|
||||||
|
Arrays.asList("secret"))));
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getHeaders())
|
||||||
|
.containsOnlyKeys(mixedCase(HttpHeaders.AUTHORIZATION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cookieHeaderIsNotIncludedByDefault() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REQUEST_HEADERS))
|
||||||
|
.receivedRequest(createRequest(Collections
|
||||||
|
.singletonMap(HttpHeaders.COOKIE, Arrays.asList("test=test"))));
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getHeaders()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mixedCaseCookieHeaderIsNotIncludedByDefault() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(EnumSet.of(Include.REQUEST_HEADERS))
|
||||||
|
.receivedRequest(createRequest(Collections.singletonMap(
|
||||||
|
mixedCase(HttpHeaders.COOKIE), Arrays.asList("valuet"))));
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getHeaders()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cookieHeaderCanBeIncluded() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(
|
||||||
|
EnumSet.of(Include.REQUEST_HEADERS, Include.COOKIE_HEADERS))
|
||||||
|
.receivedRequest(createRequest(Collections.singletonMap(
|
||||||
|
HttpHeaders.COOKIE, Arrays.asList("value"))));
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getHeaders()).containsOnlyKeys(HttpHeaders.COOKIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mixedCaseCookieHeaderCanBeIncluded() {
|
||||||
|
HttpTrace trace = new HttpExchangeTracer(
|
||||||
|
EnumSet.of(Include.REQUEST_HEADERS, Include.COOKIE_HEADERS))
|
||||||
|
.receivedRequest(createRequest(Collections.singletonMap(
|
||||||
|
mixedCase(HttpHeaders.COOKIE), Arrays.asList("value"))));
|
||||||
|
Request request = trace.getRequest();
|
||||||
|
assertThat(request.getHeaders()).containsOnlyKeys(mixedCase(HttpHeaders.COOKIE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statusIsIncluded() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace,
|
||||||
|
createResponse(), null, null);
|
||||||
|
assertThat(trace.getResponse().getStatus()).isEqualTo(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void responseHeadersAreNotIncludedByDefault() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace,
|
||||||
|
createResponse(), null, null);
|
||||||
|
assertThat(trace.getResponse().getHeaders()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void responseHeadersCanBeIncluded() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(EnumSet.of(Include.RESPONSE_HEADERS))
|
||||||
|
.sendingResponse(trace, createResponse(), null, null);
|
||||||
|
assertThat(trace.getResponse().getHeaders())
|
||||||
|
.containsOnlyKeys(HttpHeaders.CONTENT_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setCookieHeaderIsNotIncludedByDefault() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(EnumSet.of(Include.RESPONSE_HEADERS)).sendingResponse(
|
||||||
|
trace, createResponse(Collections.singletonMap(HttpHeaders.SET_COOKIE,
|
||||||
|
Arrays.asList("test=test"))),
|
||||||
|
null, null);
|
||||||
|
assertThat(trace.getResponse().getHeaders()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mixedCaseSetCookieHeaderIsNotIncludedByDefault() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(EnumSet.of(Include.RESPONSE_HEADERS))
|
||||||
|
.sendingResponse(trace,
|
||||||
|
createResponse(Collections.singletonMap(
|
||||||
|
mixedCase(HttpHeaders.SET_COOKIE),
|
||||||
|
Arrays.asList("test=test"))),
|
||||||
|
null, null);
|
||||||
|
assertThat(trace.getResponse().getHeaders()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setCookieHeaderCanBeIncluded() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(
|
||||||
|
EnumSet.of(Include.RESPONSE_HEADERS, Include.COOKIE_HEADERS))
|
||||||
|
.sendingResponse(trace,
|
||||||
|
createResponse(
|
||||||
|
Collections.singletonMap(HttpHeaders.SET_COOKIE,
|
||||||
|
Arrays.asList("test=test"))),
|
||||||
|
null, null);
|
||||||
|
assertThat(trace.getResponse().getHeaders())
|
||||||
|
.containsOnlyKeys(HttpHeaders.SET_COOKIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mixedCaseSetCookieHeaderCanBeIncluded() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(
|
||||||
|
EnumSet.of(Include.RESPONSE_HEADERS, Include.COOKIE_HEADERS))
|
||||||
|
.sendingResponse(trace,
|
||||||
|
createResponse(Collections.singletonMap(
|
||||||
|
mixedCase(HttpHeaders.SET_COOKIE),
|
||||||
|
Arrays.asList("test=test"))),
|
||||||
|
null, null);
|
||||||
|
assertThat(trace.getResponse().getHeaders())
|
||||||
|
.containsOnlyKeys(mixedCase(HttpHeaders.SET_COOKIE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void principalIsNotIncludedByDefault() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace,
|
||||||
|
createResponse(), () -> createPrincipal(), null);
|
||||||
|
assertThat(trace.getPrincipal()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void principalCanBeIncluded() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(EnumSet.of(Include.PRINCIPAL)).sendingResponse(trace,
|
||||||
|
createResponse(), () -> createPrincipal(), null);
|
||||||
|
assertThat(trace.getPrincipal()).isNotNull();
|
||||||
|
assertThat(trace.getPrincipal().getName()).isEqualTo("alice");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sessionIdIsNotIncludedByDefault() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace,
|
||||||
|
createResponse(), null, () -> "sessionId");
|
||||||
|
assertThat(trace.getSession()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sessionIdCanBeIncluded() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(EnumSet.of(Include.SESSION_ID)).sendingResponse(trace,
|
||||||
|
createResponse(), null, () -> "sessionId");
|
||||||
|
assertThat(trace.getSession()).isNotNull();
|
||||||
|
assertThat(trace.getSession().getId()).isEqualTo("sessionId");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void timeTakenIsNotIncludedByDefault() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(EnumSet.noneOf(Include.class)).sendingResponse(trace,
|
||||||
|
createResponse(), null, null);
|
||||||
|
assertThat(trace.getTimeTaken()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void timeTakedCanBeIncluded() {
|
||||||
|
HttpTrace trace = new HttpTrace(createRequest());
|
||||||
|
new HttpExchangeTracer(EnumSet.of(Include.TIME_TAKEN)).sendingResponse(trace,
|
||||||
|
createResponse(), null, null);
|
||||||
|
assertThat(trace.getTimeTaken()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TraceableRequest createRequest() {
|
||||||
|
return createRequest(Collections.singletonMap(HttpHeaders.ACCEPT,
|
||||||
|
Arrays.asList("application/json")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TraceableRequest createRequest(Map<String, List<String>> headers) {
|
||||||
|
TraceableRequest request = mock(TraceableRequest.class);
|
||||||
|
given(request.getMethod()).willReturn("GET");
|
||||||
|
given(request.getRemoteAddress()).willReturn("127.0.0.1");
|
||||||
|
given(request.getHeaders()).willReturn(new HashMap<>(headers));
|
||||||
|
given(request.getUri()).willReturn(URI.create("https://api.example.com"));
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TraceableResponse createResponse() {
|
||||||
|
return createResponse(Collections.singletonMap(HttpHeaders.CONTENT_TYPE,
|
||||||
|
Arrays.asList("application/json")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TraceableResponse createResponse(Map<String, List<String>> headers) {
|
||||||
|
TraceableResponse response = mock(TraceableResponse.class);
|
||||||
|
given(response.getStatus()).willReturn(204);
|
||||||
|
given(response.getHeaders()).willReturn(new HashMap<>(headers));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Principal createPrincipal() {
|
||||||
|
Principal principal = mock(Principal.class);
|
||||||
|
given(principal.getName()).willReturn("alice");
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String mixedCase(String input) {
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
for (int i = 0; i < input.length(); i++) {
|
||||||
|
output.append(i % 2 == 0 ? Character.toLowerCase(input.charAt(i))
|
||||||
|
: Character.toUpperCase(input.charAt(i)));
|
||||||
|
}
|
||||||
|
return output.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link InMemoryHttpTraceRepository}.
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public class InMemoryTraceRepositoryTests {
|
||||||
|
|
||||||
|
private final InMemoryHttpTraceRepository repository = new InMemoryHttpTraceRepository();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void capacityLimited() {
|
||||||
|
this.repository.setCapacity(2);
|
||||||
|
this.repository.add(new HttpTrace(createRequest("GET")));
|
||||||
|
this.repository.add(new HttpTrace(createRequest("POST")));
|
||||||
|
this.repository.add(new HttpTrace(createRequest("DELETE")));
|
||||||
|
List<HttpTrace> traces = this.repository.findAll();
|
||||||
|
assertThat(traces).hasSize(2);
|
||||||
|
assertThat(traces.get(0).getRequest().getMethod()).isEqualTo("DELETE");
|
||||||
|
assertThat(traces.get(1).getRequest().getMethod()).isEqualTo("POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reverseFalse() {
|
||||||
|
this.repository.setReverse(false);
|
||||||
|
this.repository.setCapacity(2);
|
||||||
|
this.repository.add(new HttpTrace(createRequest("GET")));
|
||||||
|
this.repository.add(new HttpTrace(createRequest("POST")));
|
||||||
|
this.repository.add(new HttpTrace(createRequest("DELETE")));
|
||||||
|
List<HttpTrace> traces = this.repository.findAll();
|
||||||
|
assertThat(traces).hasSize(2);
|
||||||
|
assertThat(traces.get(0).getRequest().getMethod()).isEqualTo("POST");
|
||||||
|
assertThat(traces.get(1).getRequest().getMethod()).isEqualTo("DELETE");
|
||||||
|
}
|
||||||
|
|
||||||
|
private TraceableRequest createRequest(String method) {
|
||||||
|
TraceableRequest request = mock(TraceableRequest.class);
|
||||||
|
given(request.getMethod()).willReturn(method);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link HttpTraceEndpoint}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public class TraceEndpointTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void trace() {
|
||||||
|
HttpTraceRepository repository = new InMemoryHttpTraceRepository();
|
||||||
|
repository.add(new HttpTrace(createRequest("GET")));
|
||||||
|
List<HttpTrace> traces = new HttpTraceEndpoint(repository).traces().getTraces();
|
||||||
|
assertThat(traces).hasSize(1);
|
||||||
|
HttpTrace trace = traces.get(0);
|
||||||
|
assertThat(trace.getRequest().getMethod()).isEqualTo("GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
private TraceableRequest createRequest(String method) {
|
||||||
|
TraceableRequest request = mock(TraceableRequest.class);
|
||||||
|
given(request.getMethod()).willReturn(method);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace.reactive;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTraceRepository;
|
||||||
|
import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository;
|
||||||
|
import org.springframework.boot.actuate.web.trace.Include;
|
||||||
|
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.server.reactive.HttpHandler;
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||||
|
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.RequestPredicates;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for {@link HttpTraceWebFilter}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public class HttpTraceWebFilterIntegrationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void traceForNotFoundResponseHas404Status() {
|
||||||
|
ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner()
|
||||||
|
.withUserConfiguration(Config.class);
|
||||||
|
runner.run((context) -> {
|
||||||
|
WebTestClient.bindToApplicationContext(context).build().get().uri("/")
|
||||||
|
.exchange().expectStatus().isNotFound();
|
||||||
|
HttpTraceRepository repository = context.getBean(HttpTraceRepository.class);
|
||||||
|
assertThat(repository.findAll()).hasSize(1);
|
||||||
|
assertThat(repository.findAll().get(0).getResponse().getStatus())
|
||||||
|
.isEqualTo(404);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void traceForMonoErrorWithRuntimeExceptionHas500Status() {
|
||||||
|
ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner()
|
||||||
|
.withUserConfiguration(Config.class);
|
||||||
|
runner.run((context) -> {
|
||||||
|
WebTestClient.bindToApplicationContext(context).build().get()
|
||||||
|
.uri("/mono-error").exchange().expectStatus()
|
||||||
|
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
HttpTraceRepository repository = context.getBean(HttpTraceRepository.class);
|
||||||
|
assertThat(repository.findAll()).hasSize(1);
|
||||||
|
assertThat(repository.findAll().get(0).getResponse().getStatus())
|
||||||
|
.isEqualTo(500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void traceForThrownRuntimeExceptionHas500Status() {
|
||||||
|
ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner()
|
||||||
|
.withUserConfiguration(Config.class);
|
||||||
|
runner.run((context) -> {
|
||||||
|
WebTestClient.bindToApplicationContext(context).build().get().uri("/thrown")
|
||||||
|
.exchange().expectStatus()
|
||||||
|
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
HttpTraceRepository repository = context.getBean(HttpTraceRepository.class);
|
||||||
|
assertThat(repository.findAll()).hasSize(1);
|
||||||
|
assertThat(repository.findAll().get(0).getResponse().getStatus())
|
||||||
|
.isEqualTo(500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFlux
|
||||||
|
static class Config {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public HttpTraceWebFilter httpTraceWebFilter(HttpTraceRepository repository) {
|
||||||
|
Set<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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace.reactive;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTrace.Session;
|
||||||
|
import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository;
|
||||||
|
import org.springframework.boot.actuate.web.trace.Include;
|
||||||
|
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||||
|
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import org.springframework.web.server.ServerWebExchangeDecorator;
|
||||||
|
import org.springframework.web.server.WebFilterChain;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link HttpTraceWebFilter}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public class HttpTraceWebFilterTests {
|
||||||
|
|
||||||
|
private final InMemoryHttpTraceRepository repository = new InMemoryHttpTraceRepository();
|
||||||
|
|
||||||
|
private final HttpExchangeTracer tracer = new HttpExchangeTracer(
|
||||||
|
EnumSet.allOf(Include.class));
|
||||||
|
|
||||||
|
private final HttpTraceWebFilter filter = new HttpTraceWebFilter(this.repository,
|
||||||
|
this.tracer, EnumSet.allOf(Include.class));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void filterTracesExchange() throws ServletException, IOException {
|
||||||
|
this.filter.filter(
|
||||||
|
MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.get("https://api.example.com")),
|
||||||
|
new WebFilterChain() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.web.trace.servlet;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpExchangeTracer;
|
||||||
|
import org.springframework.boot.actuate.web.trace.HttpTrace.Session;
|
||||||
|
import org.springframework.boot.actuate.web.trace.InMemoryHttpTraceRepository;
|
||||||
|
import org.springframework.boot.actuate.web.trace.Include;
|
||||||
|
import org.springframework.mock.web.MockFilterChain;
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link HttpTraceFilter}.
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Wallace Wadge
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Venil Noronha
|
||||||
|
* @author Stephane Nicoll
|
||||||
|
* @author Madhura Bhave
|
||||||
|
*/
|
||||||
|
public class HttpTraceFilterTests {
|
||||||
|
|
||||||
|
private final InMemoryHttpTraceRepository repository = new InMemoryHttpTraceRepository();
|
||||||
|
|
||||||
|
private final HttpExchangeTracer tracer = new HttpExchangeTracer(
|
||||||
|
EnumSet.allOf(Include.class));
|
||||||
|
|
||||||
|
private final HttpTraceFilter filter = new HttpTraceFilter(this.repository,
|
||||||
|
this.tracer);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void filterTracesExchange() throws ServletException, IOException {
|
||||||
|
this.filter.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(),
|
||||||
|
new MockFilterChain());
|
||||||
|
assertThat(this.repository.findAll()).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void filterCapturesSessionId() throws ServletException, IOException {
|
||||||
|
this.filter.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(),
|
||||||
|
new MockFilterChain(new HttpServlet() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void service(HttpServletRequest req,
|
||||||
|
HttpServletResponse resp)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
req.getSession(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}));
|
||||||
|
assertThat(this.repository.findAll()).hasSize(1);
|
||||||
|
Session session = this.repository.findAll().get(0).getSession();
|
||||||
|
assertThat(session).isNotNull();
|
||||||
|
assertThat(session.getId()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void filterCapturesPrincipal() throws ServletException, IOException {
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||||
|
Principal principal = mock(Principal.class);
|
||||||
|
given(principal.getName()).willReturn("alice");
|
||||||
|
request.setUserPrincipal(principal);
|
||||||
|
this.filter.doFilter(request, new MockHttpServletResponse(),
|
||||||
|
new MockFilterChain());
|
||||||
|
assertThat(this.repository.findAll()).hasSize(1);
|
||||||
|
org.springframework.boot.actuate.web.trace.HttpTrace.Principal tracedPrincipal = this.repository
|
||||||
|
.findAll().get(0).getPrincipal();
|
||||||
|
assertThat(tracedPrincipal).isNotNull();
|
||||||
|
assertThat(tracedPrincipal.getName()).isEqualTo("alice");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statusIsAssumedToBe500WhenChainFails()
|
||||||
|
throws ServletException, IOException {
|
||||||
|
try {
|
||||||
|
this.filter.doFilter(new MockHttpServletRequest(),
|
||||||
|
new MockHttpServletResponse(), new MockFilterChain(new HttpServlet() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void service(HttpServletRequest req,
|
||||||
|
HttpServletResponse resp)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
throw new IOException();
|
||||||
|
}
|
||||||
|
|
||||||
|
}));
|
||||||
|
fail("Filter swallowed IOException");
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
assertThat(this.repository.findAll()).hasSize(1);
|
||||||
|
assertThat(this.repository.findAll().get(0).getResponse().getStatus())
|
||||||
|
.isEqualTo(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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`).
|
||||||
[[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:
|
||||||
|`WebFilterChainProxy` (Spring Security)
|
|`WebFilterChainProxy` (Spring Security)
|
||||||
|`-100`
|
|`-100`
|
||||||
|
|
||||||
|
|`HttpTraceWebFilter`
|
||||||
|
|`Ordered.LOWEST_PRECEDENCE - 10`
|
||||||
|
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
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 {
|
||||||
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")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue