auto-config for zipkin support for spring mvc and restTemplate

This commit is contained in:
Spencer Gibb
2015-03-06 21:22:23 -07:00
parent 4ecf22b67c
commit aa1efa2f0a
10 changed files with 506 additions and 8 deletions

11
pom.xml
View File

@@ -58,6 +58,11 @@
<artifactId>brave-client</artifactId>
<version>${brave.version}</version>
</dependency>
<dependency>
<groupId>com.github.kristofa</groupId>
<artifactId>brave-impl</artifactId>
<version>${brave.version}</version>
</dependency>
<dependency>
<groupId>com.github.kristofa</groupId>
<artifactId>brave-impl-spring</artifactId>
@@ -72,6 +77,12 @@
<groupId>com.github.kristofa</groupId>
<artifactId>brave-tracefilters</artifactId>
<version>${brave.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.kristofa</groupId>

View File

@@ -1,14 +1,19 @@
package org.springframework.cloud.sleuth.sample;
import java.util.Random;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.EmbeddedServletContainerInitializedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author Spencer Gibb
@@ -17,19 +22,43 @@ import org.springframework.web.bind.annotation.RestController;
@EnableAutoConfiguration
@RestController
@Slf4j
public class SampleApplication {
public class SampleApplication implements ApplicationListener<EmbeddedServletContainerInitializedEvent> {
public static final String CLIENT_NAME = "testApp";
@Autowired
Environment env;
@Autowired
private RestTemplate restTemplate;
private int port;
@RequestMapping("/")
@SneakyThrows
@RequestMapping("/")
public String hi() {
return "hi";
final Random random = new Random();
Thread.sleep(random.nextInt(1000));
String s = restTemplate.getForObject("http://localhost:" + port + "/hi2", String.class);
return "hi/"+s;
}
@SneakyThrows
@RequestMapping("/hi2")
public String hi2() {
final Random random = new Random();
Thread.sleep(random.nextInt(1000));
return "hi2";
}
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
/*@Bean
public SpanCollector spanCollector() {
return new LoggingSpanCollectorImpl();
}*/
@Override
public void onApplicationEvent(EmbeddedServletContainerInitializedEvent event) {
port = event.getEmbeddedServletContainer().getPort();
}
}

View File

@@ -1,5 +1,5 @@
server:
port: 8080
port: 3380
spring:
application:

View File

@@ -24,6 +24,27 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.kristofa</groupId>
<artifactId>brave-client</artifactId>
</dependency>
<dependency>
<groupId>com.github.kristofa</groupId>
<artifactId>brave-impl</artifactId>
</dependency>
<dependency>
<groupId>com.github.kristofa</groupId>
<artifactId>brave-impl-spring</artifactId>
</dependency>
<dependency>
<groupId>com.github.kristofa</groupId>
<artifactId>brave-tracefilters</artifactId>
</dependency>
<dependency>
<groupId>com.github.kristofa</groupId>
<artifactId>brave-zipkin-spancollector</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>

View File

@@ -1,12 +1,86 @@
package org.springframework.cloud.sleuth.zipkin;
import java.util.List;
import com.github.kristofa.brave.zipkin.ZipkinSpanCollector;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import com.github.kristofa.brave.AnnotationSubmitterConfig;
import com.github.kristofa.brave.ClientTracer;
import com.github.kristofa.brave.ClientTracerConfig;
import com.github.kristofa.brave.EndPointSubmitterConfig;
import com.github.kristofa.brave.FixedSampleRateTraceFilter;
import com.github.kristofa.brave.LoggingSpanCollectorImpl;
import com.github.kristofa.brave.ServerSpanThreadBinderConfig;
import com.github.kristofa.brave.ServerTracerConfig;
import com.github.kristofa.brave.SpanCollector;
import com.github.kristofa.brave.TraceFilter;
import com.github.kristofa.brave.TraceFilters;
import com.github.kristofa.brave.client.ClientRequestInterceptor;
import com.github.kristofa.brave.client.ClientResponseInterceptor;
import com.github.kristofa.brave.client.spanfilter.SpanNameFilter;
import com.google.common.base.Optional;
/**
* @author Spencer Gibb
*/
@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(ServerTracerConfig.class)
@Import({ AnnotationSubmitterConfig.class, ClientTracerConfig.class,
EndPointSubmitterConfig.class, ServerSpanThreadBinderConfig.class,
ServerTracerConfig.class })
public class ZipkinAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SpanCollector spanCollector() {
return new ZipkinSpanCollector(zipkinProperties().getHost(),
zipkinProperties().getPort());
}
@Bean
public ZipkinProperties zipkinProperties() {
return new ZipkinProperties();
}
@Bean
public FixedSampleRateTraceFilter fixedSampleRateTraceFilter() {
return new FixedSampleRateTraceFilter(zipkinProperties().getFixedSampleRate());
}
@Bean
@ConditionalOnMissingBean
public TraceFilters traceFilters(List<TraceFilter> traceFilters) {
return new TraceFilters(traceFilters);
}
@Configuration
protected static class InterceptorConfig {
@Autowired
private ClientTracer clientTracer;
@Autowired(required = false)
private SpanNameFilter spanNameFilter;
@Bean
@ConditionalOnMissingBean
public ClientRequestInterceptor clientRequestInterceptor() {
return new ClientRequestInterceptor(clientTracer,
Optional.fromNullable(spanNameFilter));
}
@Bean
@ConditionalOnMissingBean
public ClientResponseInterceptor clientResponseInterceptor() {
return new ClientResponseInterceptor(clientTracer);
}
}
}

View File

@@ -0,0 +1,161 @@
package org.springframework.cloud.sleuth.zipkin;
import static com.github.kristofa.brave.BraveHttpHeaders.*;
import com.github.kristofa.brave.EndPointSubmitter;
import com.github.kristofa.brave.IdConversion;
import com.github.kristofa.brave.ServerTracer;
import lombok.Data;
import lombok.extern.apachecommons.CommonsLog;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
/**
* @author Spencer Gibb
*/
@CommonsLog
public class ZipkinHandlerInterceptor implements HandlerInterceptor {
private final EndPointSubmitter endPointSubmitter;
private final ServerTracer serverTracer;
public ZipkinHandlerInterceptor(EndPointSubmitter endPointSubmitter, ServerTracer serverTracer) {
this.endPointSubmitter = endPointSubmitter;
this.serverTracer = serverTracer;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
submitEndpoint(request);
serverTracer.clearCurrentSpan();
final TraceData traceData = getTraceData(request);
if (Boolean.FALSE.equals(traceData.getShouldBeSampled())) {
serverTracer.setStateNoTracing();
log.debug("Received indication that we should NOT trace.");
} else {
final String spanName = getSpanName(request, traceData);
if (traceData.getTraceId() != null && traceData.getSpanId() != null) {
log.debug("Received span information as part of request.");
serverTracer.setStateCurrentTrace(traceData.getTraceId(), traceData.getSpanId(),
traceData.getParentSpanId(), spanName);
} else {
log.debug("Received no span state.");
serverTracer.setStateUnknown(spanName);
}
serverTracer.setServerReceived();
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// We can submit this in any case. When server state is not set or
// we should not trace this request nothing will happen.
log.debug("Sending server send.");
try {
serverTracer.setServerSend();
} finally {
serverTracer.clearCurrentSpan();
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
private void submitEndpoint(HttpServletRequest servletRequest) {
if (!endPointSubmitter.endPointSubmitted()) {
final String localAddr = servletRequest.getLocalAddr();
final int localPort = servletRequest.getLocalPort();
final String contextPath = servletRequest.getContextPath();
log.debug("Setting endpoint: addr: "+localAddr+", port: "+localPort+", contextpath: "+ contextPath);
endPointSubmitter.submit(localAddr, localPort, contextPath);
}
}
private TraceData getTraceData(final HttpServletRequest request) {
ServletServerHttpRequest req = new ServletServerHttpRequest(request);
HttpHeaders headers = req.getHeaders();
TraceData traceData = new TraceData();
for (Map.Entry<String, List<String>> headerEntry : headers.entrySet()) {
log.debug(headerEntry.getKey() +" = "+ headerEntry.getValue());
if (TraceId.getName().equalsIgnoreCase(headerEntry.getKey())) {
traceData.setTraceId(getFirstLong(headerEntry));
} else if (SpanId.getName().equalsIgnoreCase(headerEntry.getKey())) {
traceData.setSpanId(getFirstLong(headerEntry));
} else if (ParentSpanId.getName().equalsIgnoreCase(headerEntry.getKey())) {
traceData.setParentSpanId(getFirstLong(headerEntry));
} else if (Sampled.getName().equalsIgnoreCase(headerEntry.getKey())) {
traceData.setShouldBeSampled(getFirstBoolean(headerEntry));
} else if (SpanName.getName().equalsIgnoreCase(headerEntry.getKey())) {
traceData.setSpanName(getFirstString(headerEntry));
}
}
return traceData;
}
private String getSpanName(final HttpServletRequest request, final TraceData traceData) {
if (StringUtils.isNotBlank(traceData.getSpanName())) {
return traceData.getSpanName();
} else {
//TODO: what is the functional equivalent of resteasy request.getPreprocessedPath();
UriComponents components = UriComponentsBuilder.fromUriString(request.getRequestURL().toString()).build();
StringBuilder preprocessedPath = new StringBuilder();
for (String segment : components.getPathSegments()) {
preprocessedPath.append("/").append(segment);
}
if (preprocessedPath.length() == 0) {
preprocessedPath.append("/");
}
return preprocessedPath.toString();
}
}
private Long getFirstLong(final Map.Entry<String, List<String>> headerEntry) {
final String value = getFirstString(headerEntry);
if (value == null) {
return null;
}
return IdConversion.convertToLong(value);
}
private Boolean getFirstBoolean(final Map.Entry<String, List<String>> headerEntry) {
final String firstStringValueFor = getFirstString(headerEntry);
return firstStringValueFor == null ? null : Boolean.valueOf(firstStringValueFor);
}
private String getFirstString(final Map.Entry<String, List<String>> headerEntry) {
final List<String> values = headerEntry.getValue();
if (values != null && values.size() > 0) {
return headerEntry.getValue().get(0);
}
return null;
}
@Data
class TraceData {
private Long traceId;
private Long spanId;
private Long parentSpanId;
private Boolean shouldBeSampled;
private String spanName;
}
}

View File

@@ -0,0 +1,16 @@
package org.springframework.cloud.sleuth.zipkin;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Spencer Gibb
*/
@ConfigurationProperties("spring.cloud.sleuth.zipkin")
@Data
public class ZipkinProperties {
// Sample rate = 1 means every request will get traced.
private int fixedSampleRate = 1;
private String host = "localhost";
private int port = 9410;
}

View File

@@ -0,0 +1,99 @@
package org.springframework.cloud.sleuth.zipkin;
import com.github.kristofa.brave.BraveHttpHeaders;
import com.github.kristofa.brave.ClientRequestAdapter;
import com.github.kristofa.brave.ClientResponseAdapter;
import com.github.kristofa.brave.client.ClientRequestInterceptor;
import com.github.kristofa.brave.client.ClientResponseInterceptor;
import com.google.common.base.Optional;
import lombok.SneakyThrows;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
import java.net.URI;
/**
* @author Spencer Gibb
*/
public class ZipkinRestTemplateInterceptor implements ClientHttpRequestInterceptor {
private final ClientRequestInterceptor clientRequestInterceptor;
private final ClientResponseInterceptor clientResponseInterceptor;
public ZipkinRestTemplateInterceptor(ClientRequestInterceptor clientRequestInterceptor, ClientResponseInterceptor clientResponseInterceptor) {
this.clientRequestInterceptor = clientRequestInterceptor;
//TODO: ClientResponseInterceptor assumes >= 300 is error
this.clientResponseInterceptor = clientResponseInterceptor;
}
@SneakyThrows
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
clientRequestInterceptor.handle(new RequestAdapter(request), Optional.<String>absent());
ClientHttpResponse response = null;
Exception exception = null;
try {
response = execution.execute(request, body);
} catch (final Exception e) {
exception = e;
}
clientResponseInterceptor.handle(new ResponseAdapter(response));
if(exception != null) {
throw exception;
}
return response;
}
class RequestAdapter implements ClientRequestAdapter {
HttpRequest request;
public RequestAdapter(HttpRequest request) {
this.request = request;
}
@Override
public URI getUri() {
return request.getURI();
}
@Override
public String getMethod() {
return request.getMethod().toString();
}
@Override
public Optional<String> getSpanName() {
String spanNameHeader = request.getHeaders().getFirst(BraveHttpHeaders.SpanName.getName());
return Optional.fromNullable(spanNameHeader);
}
@Override
public void addHeader(String header, String value) {
request.getHeaders().add(header, value);
}
}
class ResponseAdapter implements ClientResponseAdapter {
ClientHttpResponse response;
public ResponseAdapter(ClientHttpResponse response) {
this.response = response;
}
@SneakyThrows
@Override
public int getStatusCode() {
if (response == null) {
return 0;
}
return response.getRawStatusCode();
}
}
}

View File

@@ -0,0 +1,86 @@
package org.springframework.cloud.sleuth.zipkin.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.cloud.sleuth.zipkin.ZipkinAutoConfiguration;
import org.springframework.cloud.sleuth.zipkin.ZipkinHandlerInterceptor;
import org.springframework.cloud.sleuth.zipkin.ZipkinRestTemplateInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.github.kristofa.brave.EndPointSubmitter;
import com.github.kristofa.brave.ServerTracer;
import com.github.kristofa.brave.ServerTracerConfig;
import com.github.kristofa.brave.client.ClientRequestInterceptor;
import com.github.kristofa.brave.client.ClientResponseInterceptor;
/**
* @author Spencer Gibb
*/
@Configuration
@ConditionalOnClass(ServerTracerConfig.class)
@ConditionalOnWebApplication
@AutoConfigureAfter(ZipkinAutoConfiguration.class)
public class ZipkinWebAutoConfiguration {
@Autowired
private EndPointSubmitter endPointSubmitter;
@Autowired
private ServerTracer serverTracer;
@Bean
public ZipkinHandlerInterceptor zipkinHandlerInterceptor() {
return new ZipkinHandlerInterceptor(endPointSubmitter, serverTracer);
}
@Bean
public WebMvcConfigurerAdapter webMvcConfigurerAdapter() {
return new ZipkinWebConfigurer(zipkinHandlerInterceptor());
}
@Configuration
protected static class RestTemplateConfig {
@Autowired
private ClientRequestInterceptor clientRequestInterceptor;
@Autowired
private ClientResponseInterceptor clientResponseInterceptor;
@Bean
@ConditionalOnMissingBean
public RestTemplate restTemplate() {
//TODO: howto add this to an existing restTemplate without circular dependencies
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(zipkinRestTemplateInterceptor());
return restTemplate;
}
@Bean
public ZipkinRestTemplateInterceptor zipkinRestTemplateInterceptor() {
return new ZipkinRestTemplateInterceptor(clientRequestInterceptor,
clientResponseInterceptor);
}
}
protected static class ZipkinWebConfigurer extends WebMvcConfigurerAdapter {
private ZipkinHandlerInterceptor interceptor;
public ZipkinWebConfigurer(ZipkinHandlerInterceptor interceptor) {
this.interceptor = interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/**");
}
}
}

View File

@@ -1,3 +1,4 @@
# Auto Configuration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.sleuth.zipkin.ZipkinAutoConfiguration
org.springframework.cloud.sleuth.zipkin.ZipkinAutoConfiguration,\
org.springframework.cloud.sleuth.zipkin.web.ZipkinWebAutoConfiguration