[bs-80] Add configurable / switchable web request trace logging (headers etc)

* Added a bean post processor for the Spring Security filter chain
(so you only get traces by default if security is on)
* Every request is logged at trace level if the dump requests flag is
on
* Requests are also dumped to a TraceRepository for later analysis (very
useful for tracing problems in real time when a support call comes in)

[Fixes #48976001]
This commit is contained in:
Dave Syer
2013-04-30 13:46:46 +01:00
parent dd1fc3f992
commit 833b13bbbc
20 changed files with 652 additions and 97 deletions

View File

@@ -40,16 +40,16 @@ public class ManagementAutoConfiguration implements ApplicationContextAware,
private ApplicationContext parent;
private ConfigurableApplicationContext context;
@Autowired
private ContainerProperties configuration = new ContainerProperties();
@ConditionalOnExpression("${container.port:8080} == ${container.management_port:8080}")
@Configuration
@Import({ VarzAutoConfiguration.class, HealthzAutoConfiguration.class,
ShutdownAutoConfiguration.class })
ShutdownAutoConfiguration.class, TraceAutoConfiguration.class })
public static class ManagementEndpointsConfiguration {
}
@Autowired
private ContainerProperties configuration = new ContainerProperties();
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
@@ -73,7 +73,7 @@ public class ManagementAutoConfiguration implements ApplicationContextAware,
context.setParent(this.parent);
context.register(ManagementContainerConfiguration.class,
VarzAutoConfiguration.class, HealthzAutoConfiguration.class,
ShutdownAutoConfiguration.class);
ShutdownAutoConfiguration.class, TraceAutoConfiguration.class);
context.refresh();
this.context = context;
}

View File

@@ -16,11 +16,19 @@
package org.springframework.bootstrap.autoconfigure.service;
import java.util.List;
import org.springframework.bootstrap.context.annotation.EnableAutoConfiguration;
import org.springframework.bootstrap.service.annotation.EnableConfigurationProperties;
import org.springframework.bootstrap.service.properties.ContainerProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.joda.JodaModule;
/**
* {@link EnableAutoConfiguration Auto-configuration} for service apps.
@@ -31,7 +39,20 @@ import org.springframework.context.annotation.Import;
@Import({ ManagementAutoConfiguration.class, MetricAutoConfiguration.class,
ContainerConfiguration.class, SecurityAutoConfiguration.class,
MetricFilterAutoConfiguration.class })
public class ServiceAutoConfiguration {
public class ServiceAutoConfiguration extends WebMvcConfigurationSupport {
@Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
addDefaultHttpMessageConverters(converters);
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter) converter;
jacksonConverter.getObjectMapper().registerModule(new JodaModule());
jacksonConverter.getObjectMapper().disable(
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
}
}
/*
* ContainerProperties has to be declared in a non-conditional bean, so that it gets

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-2013 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.bootstrap.autoconfigure.service;
import javax.servlet.Servlet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.bootstrap.context.annotation.ConditionalOnClass;
import org.springframework.bootstrap.context.annotation.ConditionalOnMissingBean;
import org.springframework.bootstrap.context.annotation.EnableAutoConfiguration;
import org.springframework.bootstrap.service.properties.ContainerProperties;
import org.springframework.bootstrap.service.trace.InMemoryTraceRepository;
import org.springframework.bootstrap.service.trace.SecurityFilterPostProcessor;
import org.springframework.bootstrap.service.trace.TraceEndpoint;
import org.springframework.bootstrap.service.trace.TraceRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
/**
* {@link EnableAutoConfiguration Auto-configuration} for /trace endpoint.
*
* @author Dave Syer
*/
@Configuration
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@ConditionalOnMissingBean({ TraceEndpoint.class })
public class TraceAutoConfiguration {
@Autowired
private ContainerProperties configuration = new ContainerProperties();
@Autowired(required = false)
private TraceRepository traceRepository = new InMemoryTraceRepository();
@Bean
@ConditionalOnMissingBean({ TraceRepository.class })
protected TraceRepository traceRepository() {
return this.traceRepository;
}
@Bean
public SecurityFilterPostProcessor securityFilterPostProcessor() {
SecurityFilterPostProcessor processor = new SecurityFilterPostProcessor(
traceRepository());
processor.setDumpRequests(this.configuration.isDumpRequests());
return processor;
}
@Bean
public TraceEndpoint traceEndpoint() {
return new TraceEndpoint(this.traceRepository);
}
}

View File

@@ -19,11 +19,14 @@ package org.springframework.bootstrap.service.annotation;
import java.lang.reflect.Field;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.bootstrap.bind.PropertySourcesBindingPostProcessor;
import org.springframework.bootstrap.context.annotation.ConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
@@ -47,6 +50,10 @@ public class ConfigurationPropertiesBindingConfiguration {
@Autowired(required = false)
private Environment environment;
@Autowired(required = false)
@Qualifier(ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME)
private ConversionService conversionService;
/**
* Lifecycle hook that binds application properties to any bean whose type is
* decorated with {@link ConfigurationProperties} annotation.
@@ -72,6 +79,7 @@ public class ConfigurationPropertiesBindingConfiguration {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.afterPropertiesSet();
processor.setValidator(validator);
processor.setConversionService(this.conversionService);
processor.setPropertySources(propertySources);
return processor;
}

View File

@@ -38,6 +38,8 @@ public class ContainerProperties {
private Tomcat tomcat = new Tomcat();
private boolean dumpRequests;
public Tomcat getTomcat() {
return this.tomcat;
}
@@ -74,6 +76,14 @@ public class ContainerProperties {
this.allowShutdown = allowShutdown;
}
public boolean isDumpRequests() {
return this.dumpRequests;
}
public void setDumpRequests(boolean dumpRequests) {
this.dumpRequests = dumpRequests;
}
public static class Tomcat {
private String accessLogPattern;

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-2013 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.bootstrap.service.trace;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.joda.time.DateTime;
/**
* @author Dave Syer
*
*/
public class InMemoryTraceRepository implements TraceRepository {
private int capacity = 100;
private List<Trace> traces = new ArrayList<Trace>();
/**
* @param capacity the capacity to set
*/
public void setCapacity(int capacity) {
this.capacity = capacity;
}
@Override
public List<Trace> traces() {
synchronized (this.traces) {
return Collections.unmodifiableList(this.traces);
}
}
@Override
public void add(Map<String, Object> map) {
Trace trace = new Trace(new DateTime(), map);
synchronized (this.traces) {
while (this.traces.size() >= this.capacity) {
this.traces.remove(0);
}
this.traces.add(trace);
}
}
}

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2012-2013 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.bootstrap.service.trace;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.util.Assert;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Bean post processor that adds a filter to Spring Security. The filter (optionally) logs
* request headers at trace level and also sends the headers to a {@link TraceRepository}
* for later analysis.
*
* @author Luke Taylor
* @author Dave Syer
*
*/
public class SecurityFilterPostProcessor implements BeanPostProcessor {
private final static Log logger = LogFactory
.getLog(SecurityFilterPostProcessor.class);
private boolean dumpRequests = false;
private List<String> ignore = Collections.emptyList();
private TraceRepository traceRepository = new InMemoryTraceRepository();
/**
* @param traceRepository
*/
public SecurityFilterPostProcessor(TraceRepository traceRepository) {
super();
this.traceRepository = traceRepository;
}
/**
* List of filter chains which should be ignored completely.
*/
public void setIgnore(List<String> ignore) {
Assert.notNull(ignore);
this.ignore = ignore;
}
/**
* Debugging feature. If enabled, and trace logging is enabled
*/
public void setDumpRequests(boolean dumpRequests) {
this.dumpRequests = dumpRequests;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (!this.ignore.contains(beanName)) {
if (bean instanceof FilterChainProxy) {
FilterChainProxy proxy = (FilterChainProxy) bean;
for (SecurityFilterChain filterChain : proxy.getFilterChains()) {
processFilterChain(filterChain, beanName);
}
}
if (bean instanceof SecurityFilterChain) {
processFilterChain((SecurityFilterChain) bean, beanName);
}
}
return bean;
}
private void processFilterChain(SecurityFilterChain filterChain, String beanName) {
logger.info("Processing security filter chain " + beanName);
Filter loggingFilter = new WebRequestLoggingFilter(beanName);
filterChain.getFilters().add(0, loggingFilter);
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
class WebRequestLoggingFilter implements Filter {
final Log logger = LogFactory.getLog(WebRequestLoggingFilter.class);
private final String name;
private ObjectMapper objectMapper = new ObjectMapper();
WebRequestLoggingFilter(String name) {
this.name = name;
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
Map<String, Object> trace = getTrace(request);
@SuppressWarnings("unchecked")
Map<String, Object> headers = (Map<String, Object>) trace.get("headers");
SecurityFilterPostProcessor.this.traceRepository.add(trace);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Filter chain '" + this.name + "' processing request "
+ request.getMethod() + " " + request.getRequestURI());
if (SecurityFilterPostProcessor.this.dumpRequests) {
try {
this.logger.trace("Headers: "
+ this.objectMapper.writeValueAsString(headers));
} catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot create JSON", e);
}
}
}
chain.doFilter(request, response);
}
protected Map<String, Object> getTrace(HttpServletRequest request) {
Map<String, Object> map = new LinkedHashMap<String, Object>();
Enumeration<String> names = request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
List<String> values = Collections.list(request.getHeaders(name));
Object value = values;
if (values.size() == 1) {
value = values.get(0);
} else if (values.isEmpty()) {
value = "";
}
map.put(name, value);
}
Map<String, Object> trace = new LinkedHashMap<String, Object>();
trace.put("chain", this.name);
trace.put("method", request.getMethod());
trace.put("path", request.getRequestURI());
trace.put("headers", map);
return trace;
}
public void init(FilterConfig filterConfig) throws ServletException {
}
public void destroy() {
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2012-2013 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.bootstrap.service.trace;
import java.util.Map;
import org.joda.time.DateTime;
/**
* @author Dave Syer
*
*/
public class Trace {
private DateTime timestamp;
private Map<String, Object> info;
public Trace(DateTime timestamp, Map<String, Object> info) {
super();
this.timestamp = timestamp;
this.info = info;
}
public DateTime getTimestamp() {
return this.timestamp;
}
public Map<String, Object> getInfo() {
return this.info;
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2012-2013 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.bootstrap.service.trace;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author Dave Syer
*/
@Controller
public class TraceEndpoint {
private TraceRepository tracer;
/**
* @param tracer
*/
public TraceEndpoint(TraceRepository tracer) {
super();
this.tracer = tracer;
}
@RequestMapping("${endpoints.trace.path:/trace}")
@ResponseBody
public List<Trace> trace() {
return this.tracer.traces();
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2012-2013 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.bootstrap.service.trace;
import java.util.List;
import java.util.Map;
/**
* A repository for traces. Traces are simple documents (maps) with a timestamp, and can
* be used for analysing contextual information like HTTP headers.
*
* @author Dave Syer
*
*/
public interface TraceRepository {
List<Trace> traces();
void add(Map<String, Object> trace);
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2013 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.bootstrap.service.trace;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
/**
* @author Dave Syer
*
*/
public class InMemoryTraceRepositoryTests {
private InMemoryTraceRepository repository = new InMemoryTraceRepository();
@Test
public void capacityLimited() {
this.repository.setCapacity(2);
this.repository.add(Collections.<String, Object> singletonMap("foo", "bar"));
this.repository.add(Collections.<String, Object> singletonMap("bar", "foo"));
this.repository.add(Collections.<String, Object> singletonMap("bar", "bar"));
List<Trace> traces = this.repository.traces();
assertEquals(2, traces.size());
assertEquals("bar", traces.get(1).getInfo().get("bar"));
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2013 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.bootstrap.service.trace;
import java.util.Map;
import org.junit.Test;
import org.springframework.bootstrap.service.trace.SecurityFilterPostProcessor.WebRequestLoggingFilter;
import org.springframework.mock.web.MockHttpServletRequest;
import static org.junit.Assert.assertEquals;
/**
* @author Dave Syer
*
*/
public class SecurityFilterPostProcessorTests {
private SecurityFilterPostProcessor processor = new SecurityFilterPostProcessor(
new InMemoryTraceRepository());
@Test
public void filterDumpsRequest() {
WebRequestLoggingFilter filter = this.processor.new WebRequestLoggingFilter("foo");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.addHeader("Accept", "application/json");
Map<String, Object> trace = filter.getTrace(request);
assertEquals("GET", trace.get("method"));
assertEquals("/foo", trace.get("path"));
assertEquals("{Accept=application/json}", trace.get("headers").toString());
}
}