From aa1efa2f0ac61c166c42966d20afa0d3d3c54fa5 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Fri, 6 Mar 2015 21:22:23 -0700 Subject: [PATCH] auto-config for zipkin support for spring mvc and restTemplate --- pom.xml | 11 ++ .../sleuth/sample/SampleApplication.java | 41 ++++- .../src/main/resources/application.yml | 2 +- spring-cloud-sleuth-zipkin/pom.xml | 21 +++ .../zipkin/ZipkinAutoConfiguration.java | 74 ++++++++ .../zipkin/ZipkinHandlerInterceptor.java | 161 ++++++++++++++++++ .../cloud/sleuth/zipkin/ZipkinProperties.java | 16 ++ .../zipkin/ZipkinRestTemplateInterceptor.java | 99 +++++++++++ .../web/ZipkinWebAutoConfiguration.java | 86 ++++++++++ .../main/resources/META-INF/spring.factories | 3 +- 10 files changed, 506 insertions(+), 8 deletions(-) create mode 100644 spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinHandlerInterceptor.java create mode 100644 spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinProperties.java create mode 100644 spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinRestTemplateInterceptor.java create mode 100644 spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/web/ZipkinWebAutoConfiguration.java diff --git a/pom.xml b/pom.xml index 772572329..c3812f0b9 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,11 @@ brave-client ${brave.version} + + com.github.kristofa + brave-impl + ${brave.version} + com.github.kristofa brave-impl-spring @@ -72,6 +77,12 @@ com.github.kristofa brave-tracefilters ${brave.version} + + + org.slf4j + slf4j-log4j12 + + com.github.kristofa diff --git a/spring-cloud-sleuth-sample/src/main/java/org/springframework/cloud/sleuth/sample/SampleApplication.java b/spring-cloud-sleuth-sample/src/main/java/org/springframework/cloud/sleuth/sample/SampleApplication.java index 14afd6a9d..ace29542f 100644 --- a/spring-cloud-sleuth-sample/src/main/java/org/springframework/cloud/sleuth/sample/SampleApplication.java +++ b/spring-cloud-sleuth-sample/src/main/java/org/springframework/cloud/sleuth/sample/SampleApplication.java @@ -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 { 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(); + } } diff --git a/spring-cloud-sleuth-sample/src/main/resources/application.yml b/spring-cloud-sleuth-sample/src/main/resources/application.yml index 0547869c1..9dafd9d4e 100644 --- a/spring-cloud-sleuth-sample/src/main/resources/application.yml +++ b/spring-cloud-sleuth-sample/src/main/resources/application.yml @@ -1,5 +1,5 @@ server: - port: 8080 + port: 3380 spring: application: diff --git a/spring-cloud-sleuth-zipkin/pom.xml b/spring-cloud-sleuth-zipkin/pom.xml index 4715806dd..0c257652c 100644 --- a/spring-cloud-sleuth-zipkin/pom.xml +++ b/spring-cloud-sleuth-zipkin/pom.xml @@ -24,6 +24,27 @@ org.springframework.boot spring-boot-starter-actuator + true + + + com.github.kristofa + brave-client + + + com.github.kristofa + brave-impl + + + com.github.kristofa + brave-impl-spring + + + com.github.kristofa + brave-tracefilters + + + com.github.kristofa + brave-zipkin-spancollector org.projectlombok diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinAutoConfiguration.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinAutoConfiguration.java index dc9e7d965..f80ef8dd3 100644 --- a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinAutoConfiguration.java +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinAutoConfiguration.java @@ -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 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); + } + } } diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinHandlerInterceptor.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinHandlerInterceptor.java new file mode 100644 index 000000000..585f5f203 --- /dev/null +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinHandlerInterceptor.java @@ -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> 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> headerEntry) { + final String value = getFirstString(headerEntry); + if (value == null) { + return null; + } + return IdConversion.convertToLong(value); + + } + + private Boolean getFirstBoolean(final Map.Entry> headerEntry) { + final String firstStringValueFor = getFirstString(headerEntry); + return firstStringValueFor == null ? null : Boolean.valueOf(firstStringValueFor); + } + + private String getFirstString(final Map.Entry> headerEntry) { + final List 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; + } +} diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinProperties.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinProperties.java new file mode 100644 index 000000000..ce170fd63 --- /dev/null +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinProperties.java @@ -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; +} diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinRestTemplateInterceptor.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinRestTemplateInterceptor.java new file mode 100644 index 000000000..718feae85 --- /dev/null +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinRestTemplateInterceptor.java @@ -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.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 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(); + } + } +} diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/web/ZipkinWebAutoConfiguration.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/web/ZipkinWebAutoConfiguration.java new file mode 100644 index 000000000..efa048c0a --- /dev/null +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/web/ZipkinWebAutoConfiguration.java @@ -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("/**"); + } + } +} diff --git a/spring-cloud-sleuth-zipkin/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-zipkin/src/main/resources/META-INF/spring.factories index 0fca7baa9..ef706534a 100644 --- a/spring-cloud-sleuth-zipkin/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-zipkin/src/main/resources/META-INF/spring.factories @@ -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