[#188][#194] Feign and RestTemplate create a new Span

fixes #188
fixes #194
This commit is contained in:
Marcin Grzejszczak
2016-03-01 12:16:24 +01:00
parent 1cb3ec78eb
commit 57b0f14d9a
10 changed files with 201 additions and 66 deletions

View File

@@ -16,8 +16,10 @@
package org.springframework.cloud.sleuth.instrument.web.client;
import java.net.URI;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanAccessor;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.event.ClientReceivedEvent;
import org.springframework.cloud.sleuth.event.ClientSentEvent;
import org.springframework.context.ApplicationEvent;
@@ -38,10 +40,10 @@ abstract class AbstractTraceHttpRequestInterceptor
implements ApplicationEventPublisherAware {
private ApplicationEventPublisher publisher;
private final SpanAccessor accessor;
protected final Tracer tracer;
protected AbstractTraceHttpRequestInterceptor(SpanAccessor accessor) {
this.accessor = accessor;
protected AbstractTraceHttpRequestInterceptor(Tracer tracer) {
this.tracer = tracer;
}
@Override
@@ -70,7 +72,7 @@ abstract class AbstractTraceHttpRequestInterceptor
private void setHeader(HttpRequest request, String name, String value) {
if (StringUtils.hasText(value) && !request.getHeaders().containsKey(name) &&
this.accessor.isTracing()) {
this.tracer.isTracing()) {
request.getHeaders().add(name, value);
}
}
@@ -86,9 +88,11 @@ abstract class AbstractTraceHttpRequestInterceptor
* the client sent event
*/
protected void publishStartEvent(HttpRequest request) {
Span span = currentSpan();
enrichWithTraceHeaders(request, span);
publish(new ClientSentEvent(this, span));
URI uri = request.getURI();
String spanName = uri.getScheme() + ":" + uri.getPath();
Span newSpan = this.tracer.startTrace(spanName);
enrichWithTraceHeaders(request, newSpan);
publish(new ClientSentEvent(this, newSpan));
}
/**
@@ -99,6 +103,7 @@ abstract class AbstractTraceHttpRequestInterceptor
return;
}
publish(new ClientReceivedEvent(this, currentSpan()));
this.tracer.close(this.currentSpan());
}
private void publish(ApplicationEvent event) {
@@ -108,11 +113,11 @@ abstract class AbstractTraceHttpRequestInterceptor
}
private Span currentSpan() {
return this.accessor.getCurrentSpan();
return this.tracer.getCurrentSpan();
}
protected boolean isTracing() {
return this.accessor.isTracing();
return this.tracer.isTracing();
}
}

View File

@@ -19,7 +19,6 @@ package org.springframework.cloud.sleuth.instrument.web.client;
import java.io.IOException;
import java.net.URI;
import org.springframework.cloud.sleuth.SpanAccessor;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.http.HttpMethod;
@@ -42,8 +41,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
public class TraceAsyncClientHttpRequestFactoryWrapper extends AbstractTraceHttpRequestInterceptor
implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory {
private final Tracer tracer;
private final AsyncClientHttpRequestFactory delegate;
private final AsyncClientHttpRequestFactory asyncDelegate;
private final ClientHttpRequestFactory syncDelegate;
/**
@@ -55,32 +53,29 @@ public class TraceAsyncClientHttpRequestFactoryWrapper extends AbstractTraceHttp
*
* @see org.springframework.web.client.AsyncRestTemplate#AsyncRestTemplate(AsyncClientHttpRequestFactory)
*/
public TraceAsyncClientHttpRequestFactoryWrapper(SpanAccessor accessor, Tracer tracer,
AsyncClientHttpRequestFactory delegate) {
super(accessor);
this.tracer = tracer;
this.delegate = delegate;
this.syncDelegate = delegate instanceof ClientHttpRequestFactory ?
(ClientHttpRequestFactory) delegate : defaultClientHttpRequestFactory();
public TraceAsyncClientHttpRequestFactoryWrapper(Tracer tracer,
AsyncClientHttpRequestFactory asyncDelegate) {
super(tracer);
this.asyncDelegate = asyncDelegate;
this.syncDelegate = asyncDelegate instanceof ClientHttpRequestFactory ?
(ClientHttpRequestFactory) asyncDelegate : defaultClientHttpRequestFactory();
}
/**
* Default implementation that creates a {@link SimpleClientHttpRequestFactory} that
* has a wrapped task executor via the {@link TraceAsyncListenableTaskExecutor}
*/
public TraceAsyncClientHttpRequestFactoryWrapper(SpanAccessor accessor, Tracer tracer) {
super(accessor);
this.tracer = tracer;
public TraceAsyncClientHttpRequestFactoryWrapper(Tracer tracer) {
super(tracer);
SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = defaultClientHttpRequestFactory();
this.delegate = simpleClientHttpRequestFactory;
this.asyncDelegate = simpleClientHttpRequestFactory;
this.syncDelegate = simpleClientHttpRequestFactory;
}
public TraceAsyncClientHttpRequestFactoryWrapper(SpanAccessor accessor, Tracer tracer,
AsyncClientHttpRequestFactory delegate, ClientHttpRequestFactory syncDelegate) {
super(accessor);
this.tracer = tracer;
this.delegate = delegate;
public TraceAsyncClientHttpRequestFactoryWrapper(Tracer tracer,
AsyncClientHttpRequestFactory asyncDelegate, ClientHttpRequestFactory syncDelegate) {
super(tracer);
this.asyncDelegate = asyncDelegate;
this.syncDelegate = syncDelegate;
}
@@ -99,7 +94,8 @@ public class TraceAsyncClientHttpRequestFactoryWrapper extends AbstractTraceHttp
@Override
public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod)
throws IOException {
AsyncClientHttpRequest request = this.delegate.createAsyncRequest(uri, httpMethod);
AsyncClientHttpRequest request = this.asyncDelegate
.createAsyncRequest(uri, httpMethod);
if (!isTracing()) {
doNotSampleThisSpan(request);
return request;

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2013-2016 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.cloud.sleuth.instrument.web.client;
import java.net.URI;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.event.ClientReceivedEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.AsyncClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.client.AsyncRequestCallback;
import org.springframework.web.client.AsyncRestTemplate;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
/**
* An {@link AsyncRestTemplate} that closes started spans when a response has been
* successfully received.
*
* @author Marcin Grzejszczak
*
* @since 1.0.0
*/
class TraceAsyncRestTemplate extends AsyncRestTemplate
implements ApplicationEventPublisherAware {
private ApplicationEventPublisher publisher;
private final Tracer tracer;
public TraceAsyncRestTemplate(Tracer tracer) {
super();
this.tracer = tracer;
}
public TraceAsyncRestTemplate(AsyncListenableTaskExecutor taskExecutor, Tracer tracer) {
super(taskExecutor);
this.tracer = tracer;
}
public TraceAsyncRestTemplate(AsyncClientHttpRequestFactory asyncRequestFactory,
Tracer tracer) {
super(asyncRequestFactory);
this.tracer = tracer;
}
public TraceAsyncRestTemplate(AsyncClientHttpRequestFactory asyncRequestFactory,
ClientHttpRequestFactory syncRequestFactory, Tracer tracer) {
super(asyncRequestFactory, syncRequestFactory);
this.tracer = tracer;
}
public TraceAsyncRestTemplate(AsyncClientHttpRequestFactory requestFactory,
RestTemplate restTemplate, Tracer tracer) {
super(requestFactory, restTemplate);
this.tracer = tracer;
}
@Override
protected <T> ListenableFuture<T> doExecute(URI url, HttpMethod method,
AsyncRequestCallback requestCallback, ResponseExtractor<T> responseExtractor)
throws RestClientException {
try {
return super.doExecute(url, method, requestCallback, responseExtractor);
} finally {
finish();
}
}
private void finish() {
if (!isTracing()) {
return;
}
publish(new ClientReceivedEvent(this, currentSpan()));
this.tracer.close(this.currentSpan());
}
private void publish(ApplicationEvent event) {
if (this.publisher != null) {
this.publisher.publishEvent(event);
}
}
private Span currentSpan() {
return this.tracer.getCurrentSpan();
}
private boolean isTracing() {
return this.tracer.isTracing();
}
@Override
public void setApplicationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
}

View File

@@ -20,6 +20,7 @@ import static java.util.Collections.singletonList;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@@ -36,7 +37,6 @@ import org.springframework.cloud.netflix.feign.FeignAutoConfiguration;
import org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder;
import org.springframework.cloud.netflix.feign.support.SpringDecoder;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanAccessor;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.event.ClientReceivedEvent;
import org.springframework.cloud.sleuth.event.ClientSentEvent;
@@ -84,7 +84,7 @@ public class TraceFeignClientAutoConfiguration {
private ApplicationEventPublisher publisher;
@Autowired
private SpanAccessor accessor;
private Tracer tracer;
@Bean
@Scope("prototype")
@@ -112,6 +112,7 @@ public class TraceFeignClientAutoConfiguration {
Span span = getCurrentSpan();
if (span != null) {
publish(new ClientReceivedEvent(this, span));
TraceFeignClientAutoConfiguration.this.tracer.close(span);
}
}
}
@@ -123,7 +124,9 @@ public class TraceFeignClientAutoConfiguration {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
Span span = getCurrentSpan();
URI uri = URI.create(template.url());
String spanName = uri.getScheme() + ":" + uri.getPath();
Span span = TraceFeignClientAutoConfiguration.this.tracer.startTrace(spanName);
if (span == null) {
setHeader(template, Span.NOT_SAMPLED_NAME, "true");
return;
@@ -156,7 +159,7 @@ public class TraceFeignClientAutoConfiguration {
public void setHeader(RequestTemplate request, String name, String value) {
if (StringUtils.hasText(value) && !request.headers().containsKey(name)
&& this.accessor.isTracing()) {
&& this.tracer.isTracing()) {
request.header(name, value);
}
}
@@ -179,7 +182,7 @@ public class TraceFeignClientAutoConfiguration {
public void setHeader(Map<String, Collection<String>> headers, String name,
String value) {
if (StringUtils.hasText(value) && !headers.containsKey(name)
&& this.accessor.isTracing()) {
&& this.tracer.isTracing()) {
headers.put(name, singletonList(value));
}
}
@@ -192,7 +195,7 @@ public class TraceFeignClientAutoConfiguration {
}
private Span getCurrentSpan() {
return this.accessor.getCurrentSpan();
return this.tracer.getCurrentSpan();
}
}

View File

@@ -17,7 +17,7 @@ package org.springframework.cloud.sleuth.instrument.web.client;
import java.io.IOException;
import org.springframework.cloud.sleuth.SpanAccessor;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
@@ -31,15 +31,14 @@ import org.springframework.http.client.ClientHttpResponse;
* @author Spencer Gibb
*
* @see org.springframework.web.client.RestTemplate
* @see SpanAccessor
*
* @since 1.0.0
*/
public class TraceRestTemplateInterceptor extends AbstractTraceHttpRequestInterceptor
implements ClientHttpRequestInterceptor {
public TraceRestTemplateInterceptor(SpanAccessor accessor) {
super(accessor);
public TraceRestTemplateInterceptor(Tracer tracer) {
super(tracer);
}
@Override

View File

@@ -47,14 +47,15 @@ public class TraceWebAsyncClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AsyncClientHttpRequestFactory asyncClientHttpRequestFactory(SpanAccessor spanAccessor, Tracer tracer) {
return new TraceAsyncClientHttpRequestFactoryWrapper(spanAccessor, tracer);
public AsyncClientHttpRequestFactory asyncClientHttpRequestFactory(Tracer tracer) {
return new TraceAsyncClientHttpRequestFactoryWrapper(tracer);
}
@Bean
@ConditionalOnMissingBean
public AsyncRestTemplate asyncRestTemplate(AsyncClientHttpRequestFactory asyncClientHttpRequestFactory) {
return new AsyncRestTemplate(asyncClientHttpRequestFactory);
public AsyncRestTemplate asyncRestTemplate(AsyncClientHttpRequestFactory asyncClientHttpRequestFactory,
Tracer tracer) {
return new TraceAsyncRestTemplate(asyncClientHttpRequestFactory, tracer);
}
}

View File

@@ -28,7 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.sleuth.SpanAccessor;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -46,14 +46,14 @@ import org.springframework.web.client.RestTemplate;
@Configuration
@ConditionalOnProperty(value = "spring.sleuth.client.enabled", matchIfMissing = true)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(SpanAccessor.class)
@ConditionalOnBean(Tracer.class)
@AutoConfigureAfter(TraceAutoConfiguration.class)
public class TraceWebClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public TraceRestTemplateInterceptor traceRestTemplateInterceptor(SpanAccessor accessor) {
return new TraceRestTemplateInterceptor(accessor);
public TraceRestTemplateInterceptor traceRestTemplateInterceptor(Tracer tracer) {
return new TraceRestTemplateInterceptor(tracer);
}
@Bean

View File

@@ -90,15 +90,16 @@ public class FeignTraceTests {
public void shouldAttachTraceIdWhenUsingFeignClient() {
Long currentTraceId = 1L;
Long currentParentId = 2L;
Long currentSpanId = generatedId();
this.tracer.continueSpan(Span.builder().traceId(currentTraceId)
.spanId(generatedId()).parent(currentParentId).build());
.spanId(currentSpanId).parent(currentParentId).build());
ResponseEntity<String> response = this.testFeignInterface.getTraceId();
then(Span.hexToId(getHeader(response, Span.TRACE_ID_NAME)))
.isEqualTo(currentTraceId);
then(Span.hexToId(getHeader(response, Span.PARENT_ID_NAME)))
.isEqualTo(currentParentId);
.isEqualTo(currentSpanId);
then(this.listener.getEvents().size()).isEqualTo(2);
}

View File

@@ -80,9 +80,9 @@ public class TraceRestTemplateInterceptorTests {
@SuppressWarnings("unchecked")
Map<String, String> headers = this.template.getForEntity("/", Map.class)
.getBody();
then(Long.valueOf(headers.get(Span.TRACE_ID_NAME))).isEqualTo(1L);
then(Long.valueOf(headers.get(Span.SPAN_ID_NAME))).isEqualTo(2L);
then(Long.valueOf(headers.get(Span.PARENT_ID_NAME))).isEqualTo(3L);
then(Span.hexToId(headers.get(Span.TRACE_ID_NAME))).isEqualTo(1L);
then(Span.hexToId(headers.get(Span.SPAN_ID_NAME))).isNotEqualTo(2L);
then(Span.hexToId(headers.get(Span.PARENT_ID_NAME))).isEqualTo(2L);
}
@Test
@@ -91,8 +91,8 @@ public class TraceRestTemplateInterceptorTests {
@SuppressWarnings("unchecked")
Map<String, String> headers = this.template.getForEntity("/", Map.class)
.getBody();
then(Long.valueOf(headers.get(Span.TRACE_ID_NAME))).isEqualTo(1L);
then(Long.valueOf(headers.get(Span.SPAN_ID_NAME))).isEqualTo(2L);
then(Span.hexToId(headers.get(Span.TRACE_ID_NAME))).isEqualTo(1L);
then(Span.hexToId(headers.get(Span.SPAN_ID_NAME))).isNotEqualTo(2L);
then(headers.get(Span.NOT_SAMPLED_NAME)).isEqualTo("true");
}

View File

@@ -16,6 +16,7 @@
package integration;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Random;
@@ -106,22 +107,32 @@ public class MessagingApplicationTests extends AbstractIntegrationTest {
private void thenTheSpansHaveProperParentStructure() {
Optional<Span> firstHttpSpan = findFirstHttpRequestSpan();
List<Span> eventSpans = findAllEventRelatedSpans();
Optional<Span> eventSentSpan = findSpanWithAnnotation(eventSpans, Constants.SERVER_SEND);
Optional<Span> eventReceivedSpan = findSpanWithAnnotation(eventSpans, Constants.CLIENT_RECV);
Optional<Span> lastHttpSpan = findLastHttpSpan();
thenAllSpansArePresent(firstHttpSpan, eventSpans, lastHttpSpan, eventSentSpan, eventReceivedSpan);
then(lastHttpSpan.get().parentId).isEqualTo(eventSentSpan.get().id);
then(eventSentSpan.get().parentId).isEqualTo(firstHttpSpan.get().id);
then(eventSentSpan.get()).isNotEqualTo(eventReceivedSpan.get());
Optional<Span> eventSentSpan = findSpanWithAnnotation(Constants.SERVER_SEND);
Optional<Span> eventReceivedSpan = findSpanWithAnnotation(Constants.CLIENT_RECV);
Optional<Span> lastHttpSpansParent = findLastHttpSpansParent();
thenAllSpansArePresent(firstHttpSpan, eventSpans, lastHttpSpansParent, eventSentSpan, eventReceivedSpan);
// "http:/parent/" -> "http:/" -> "message:messages" -> "http:/foo" (CS + CR) -> "http:/foo" (SS) -> "http:/foo"
Collections.sort(this.integrationTestSpanCollector.hashedSpans, (s1, s2) -> s1.timestamp.compareTo(s2.timestamp));
then(this.integrationTestSpanCollector.hashedSpans).hasSize(6);
for (int i=0; i<this.integrationTestSpanCollector.hashedSpans.size(); i++) {
if (i - 1 >= 0) {
Span parent = this.integrationTestSpanCollector.hashedSpans.get(i - 1);
Span current = this.integrationTestSpanCollector.hashedSpans.get(i);
// there is a pair of spans having cs/cr and ss/sr
if (current.id != parent.id) {
then(current.parentId).isEqualTo(parent.id);
}
}
}
}
private Optional<Span> findLastHttpSpan() {
private Optional<Span> findLastHttpSpansParent() {
return this.integrationTestSpanCollector.hashedSpans.stream()
.filter(span -> "http:/foo".equals(span.name)).findFirst();
.filter(span -> "http:/foo".equals(span.name) && !span.annotations.isEmpty()).findFirst();
}
private Optional<Span> findSpanWithAnnotation(List<Span> eventSpans, String annotationName) {
return eventSpans.stream()
private Optional<Span> findSpanWithAnnotation(String annotationName) {
return this.integrationTestSpanCollector.hashedSpans.stream()
.filter(span -> span.annotations.stream().filter(annotation -> annotationName
.equals(annotation.value)).findFirst().isPresent())
.findFirst();