Merge branch '2.2.x'

This commit is contained in:
Marcin Grzejszczak
2020-02-18 12:23:14 +01:00
16 changed files with 415 additions and 439 deletions

View File

@@ -33,7 +33,7 @@
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<spring-boot.version>2.3.0.BUILD-SNAPSHOT</spring-boot.version>
<brave.version>5.9.3</brave.version>
<brave.version>5.9.5</brave.version>
<okhttp.version>3.14.6</okhttp.version>
</properties>

View File

@@ -30,6 +30,7 @@ import reactor.core.Scannable;
import reactor.core.publisher.Operators;
import reactor.util.context.Context;
import org.springframework.cloud.sleuth.internal.LazyBean;
import org.springframework.context.ConfigurableApplicationContext;
/**
@@ -70,8 +71,8 @@ public abstract class ReactorSleuth {
// keep a reference outside the lambda so that any caching will be visible to
// all publishers
LazyBean<CurrentTraceContext> lazyCurrentTraceContext = new LazyBean<>(
springContext, CurrentTraceContext.class);
LazyBean<CurrentTraceContext> lazyCurrentTraceContext = LazyBean
.create(springContext, CurrentTraceContext.class);
return Operators.liftPublisher((p, sub) -> {
// We don't scope scalar results as they happen in an instant. This prevents

View File

@@ -54,7 +54,9 @@ final class ScopePassingSpanSubscriber<T> implements SpanSubscription<T>, Scanna
this.subscriber = subscriber;
this.currentTraceContext = currentTraceContext;
this.parent = parent;
this.context = parent != null ? ctx.put(TraceContext.class, parent) : ctx;
this.context = parent != null
&& !parent.equals(ctx.getOrDefault(TraceContext.class, null))
? ctx.put(TraceContext.class, parent) : ctx;
if (log.isTraceEnabled()) {
log.trace("Parent span [" + parent + "], context [" + this.context + "]");
}

View File

@@ -16,70 +16,94 @@
package org.springframework.cloud.sleuth.instrument.web.client;
import java.util.List;
import java.net.InetSocketAddress;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import brave.Span;
import brave.Tracer;
import brave.http.HttpClientHandler;
import brave.http.HttpTracing;
import brave.propagation.CurrentTraceContext;
import brave.propagation.CurrentTraceContext.Scope;
import brave.propagation.TraceContext;
import io.netty.bootstrap.Bootstrap;
import reactor.core.publisher.Mono;
import reactor.netty.Connection;
import reactor.netty.http.client.HttpClient;
import reactor.netty.http.client.HttpClientRequest;
import reactor.netty.http.client.HttpClientResponse;
import reactor.util.context.Context;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cloud.sleuth.internal.LazyBean;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.Nullable;
class HttpClientBeanPostProcessor implements BeanPostProcessor {
private final BeanFactory beanFactory;
final ConfigurableApplicationContext springContext;
HttpClientBeanPostProcessor(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
HttpClientBeanPostProcessor(ConfigurableApplicationContext springContext) {
this.springContext = springContext;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
LazyBean<HttpTracing> httpTracing = LazyBean.create(this.springContext,
HttpTracing.class);
if (bean instanceof HttpClient) {
return ((HttpClient) bean).mapConnect(new TracingMapConnect(this.beanFactory))
.doOnRequest(TracingDoOnRequest.create(this.beanFactory))
.doOnRequestError(TracingDoOnErrorRequest.create(this.beanFactory))
.doOnResponse(TracingDoOnResponse.create(this.beanFactory))
.doOnResponseError(TracingDoOnErrorResponse.create(this.beanFactory));
// This adds handlers to manage the span lifecycle. All require explicit
// propagation of the current span as a reactor context property.
// This done in mapConnect, added last so that it is setup first.
// https://projectreactor.io/docs/core/release/reference/#_simple_context_examples
return ((HttpClient) bean)
.doOnResponseError(new TracingDoOnErrorResponse(httpTracing))
.doOnResponse(new TracingDoOnResponse(httpTracing))
.doOnRequestError(new TracingDoOnErrorRequest(httpTracing))
.doOnRequest(new TracingDoOnRequest(httpTracing))
.mapConnect(new TracingMapConnect(httpTracing));
}
return bean;
}
/** current client span, cleared on completion. */
private static final class CurrentClientSpan extends AtomicReference<Span> {
}
private static class TracingMapConnect implements
BiFunction<Mono<? extends Connection>, Bootstrap, Mono<? extends Connection>> {
private final BeanFactory beanFactory;
final LazyBean<HttpTracing> httpTracing;
private Tracer tracer;
CurrentTraceContext currentTraceContext;
TracingMapConnect(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
TracingMapConnect(LazyBean<HttpTracing> httpTracing) {
this.httpTracing = httpTracing;
}
@Override
public Mono<? extends Connection> apply(Mono<? extends Connection> mono,
Bootstrap bootstrap) {
return mono.subscriberContext(context -> context.put(AtomicReference.class,
new AtomicReference<>(tracer().currentSpan())));
return mono.subscriberContext(context -> {
TraceContext invocationContext = currentTraceContext().get();
if (invocationContext != null) {
// Read in this processor and also in ScopePassingSpanSubscriber
context = context.put(TraceContext.class, invocationContext);
}
return context.put(CurrentClientSpan.class, new CurrentClientSpan());
});
}
private Tracer tracer() {
if (this.tracer == null) {
this.tracer = this.beanFactory.getBean(Tracer.class);
CurrentTraceContext currentTraceContext() {
if (this.currentTraceContext == null) {
this.currentTraceContext = this.httpTracing.get().tracing()
.currentTraceContext();
}
return this.tracer;
return this.currentTraceContext;
}
}
@@ -87,59 +111,64 @@ class HttpClientBeanPostProcessor implements BeanPostProcessor {
private static class TracingDoOnRequest
implements BiConsumer<HttpClientRequest, Connection> {
final BeanFactory beanFactory;
HttpTracing httpTracing;
List<String> propagationKeys;
final LazyBean<HttpTracing> httpTracing;
HttpClientHandler<brave.http.HttpClientRequest, brave.http.HttpClientResponse> handler;
TracingDoOnRequest(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
TracingDoOnRequest(LazyBean<HttpTracing> httpTracing) {
this.httpTracing = httpTracing;
}
static TracingDoOnRequest create(BeanFactory beanFactory) {
return new TracingDoOnRequest(beanFactory);
}
private HttpTracing httpTracing() {
if (this.httpTracing == null) {
this.httpTracing = this.beanFactory.getBean(HttpTracing.class);
}
return this.httpTracing;
}
private List<String> propagationKeys() {
if (this.propagationKeys == null) {
this.propagationKeys = httpTracing().tracing().propagation().keys();
}
return this.propagationKeys;
}
private HttpClientHandler<brave.http.HttpClientRequest, brave.http.HttpClientResponse> handler() {
HttpClientHandler<brave.http.HttpClientRequest, brave.http.HttpClientResponse> handler() {
if (this.handler == null) {
this.handler = HttpClientHandler.create(httpTracing());
this.handler = HttpClientHandler.create(httpTracing.get());
}
return this.handler;
}
CurrentTraceContext currentTraceContext() {
return httpTracing.get().tracing().currentTraceContext();
}
@Override
public void accept(HttpClientRequest req, Connection connection) {
// request already instrumented
// TODO: consider another, cheaper way, like flagging a context
// property. If not, comment why.
for (String key : propagationKeys()) {
if (req.requestHeaders().contains(key)) {
return;
}
CurrentClientSpan ref = req.currentContext()
.getOrDefault(CurrentClientSpan.class, null);
if (ref == null) { // Somehow TracingMapConnect was not invoked.. skip out
return;
}
AtomicReference<Span> reference = req.currentContext()
.getOrDefault(AtomicReference.class, new AtomicReference<>());
// This might be re-entrant on auto-redirect or connection retry:
// See reactor/reactor-netty#1000 for follow-ups.
Span clientSpan = ref.getAndSet(null);
if (clientSpan != null) {
// Retry from a connect fail wouldn't have parsed the request, leading to
// an empty span with no data if we finished it. An auto-redirect would
// have parsed the request, but we have no idea which status code it
// finished with. Since we can't see the preceding request state, we
// abandon its span in favor of the next.
clientSpan.abandon();
}
// Start a new client span with the appropriate parent
TraceContext parent = req.currentContext().getOrDefault(TraceContext.class,
null);
WrappedHttpClientRequest request = new WrappedHttpClientRequest(req);
Span span = reference.get() == null ? handler().handleSend(request)
: handler().handleSend(request, reference.get());
reference.set(span);
// Simplify after openzipkin/brave#1082
try (Scope ws = currentTraceContext().maybeScope(parent)) {
clientSpan = handler().handleSend(request);
parseConnectionAddress(connection, clientSpan);
ref.set(clientSpan);
}
}
static void parseConnectionAddress(Connection connection, Span span) {
if (span.isNoop()) {
return;
}
InetSocketAddress socketAddress = connection.address();
span.remoteIpAndPort(socketAddress.getHostString(), socketAddress.getPort());
}
}
@@ -147,17 +176,13 @@ class HttpClientBeanPostProcessor implements BeanPostProcessor {
private static class TracingDoOnResponse extends AbstractTracingDoOnHandler
implements BiConsumer<HttpClientResponse, Connection> {
TracingDoOnResponse(BeanFactory beanFactory) {
super(beanFactory);
}
static TracingDoOnResponse create(BeanFactory beanFactory) {
return new TracingDoOnResponse(beanFactory);
TracingDoOnResponse(LazyBean<HttpTracing> httpTracing) {
super(httpTracing);
}
@Override
public void accept(HttpClientResponse httpClientResponse, Connection connection) {
handle(httpClientResponse, null);
public void accept(HttpClientResponse response, Connection connection) {
handle(response.currentContext(), response, null);
}
}
@@ -165,17 +190,13 @@ class HttpClientBeanPostProcessor implements BeanPostProcessor {
private static class TracingDoOnErrorRequest extends AbstractTracingDoOnHandler
implements BiConsumer<HttpClientRequest, Throwable> {
TracingDoOnErrorRequest(BeanFactory beanFactory) {
super(beanFactory);
}
static TracingDoOnErrorRequest create(BeanFactory beanFactory) {
return new TracingDoOnErrorRequest(beanFactory);
TracingDoOnErrorRequest(LazyBean<HttpTracing> httpTracing) {
super(httpTracing);
}
@Override
public void accept(HttpClientRequest request, Throwable throwable) {
handle(null, throwable);
public void accept(HttpClientRequest req, Throwable error) {
handle(req.currentContext(), null, error);
}
}
@@ -183,59 +204,48 @@ class HttpClientBeanPostProcessor implements BeanPostProcessor {
private static class TracingDoOnErrorResponse extends AbstractTracingDoOnHandler
implements BiConsumer<HttpClientResponse, Throwable> {
TracingDoOnErrorResponse(BeanFactory beanFactory) {
super(beanFactory);
}
static TracingDoOnErrorResponse create(BeanFactory beanFactory) {
return new TracingDoOnErrorResponse(beanFactory);
TracingDoOnErrorResponse(LazyBean<HttpTracing> httpTracing) {
super(httpTracing);
}
@Override
public void accept(HttpClientResponse httpClientResponse, Throwable throwable) {
handle(httpClientResponse, throwable);
public void accept(HttpClientResponse response, Throwable error) {
handle(response.currentContext(), response, error);
}
}
private static abstract class AbstractTracingDoOnHandler {
final BeanFactory beanFactory;
HttpTracing httpTracing;
final LazyBean<HttpTracing> httpTracing;
HttpClientHandler<brave.http.HttpClientRequest, brave.http.HttpClientResponse> handler;
AbstractTracingDoOnHandler(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
AbstractTracingDoOnHandler(LazyBean<HttpTracing> httpTracing) {
this.httpTracing = httpTracing;
}
private HttpTracing httpTracing() {
if (this.httpTracing == null) {
this.httpTracing = this.beanFactory.getBean(HttpTracing.class);
}
return this.httpTracing;
}
private HttpClientHandler<brave.http.HttpClientRequest, brave.http.HttpClientResponse> handler() {
HttpClientHandler<brave.http.HttpClientRequest, brave.http.HttpClientResponse> handler() {
if (this.handler == null) {
this.handler = HttpClientHandler.create(httpTracing());
this.handler = HttpClientHandler.create(httpTracing.get());
}
return this.handler;
}
protected void handle(HttpClientResponse httpClientResponse,
Throwable throwable) {
if (httpClientResponse == null) {
void handle(Context context, @Nullable HttpClientResponse resp,
@Nullable Throwable error) {
CurrentClientSpan ref = context.getOrDefault(CurrentClientSpan.class, null);
if (ref == null) { // Somehow TracingMapConnect was not invoked.. skip out
return;
}
AtomicReference reference = httpClientResponse.currentContext()
.getOrDefault(AtomicReference.class, null);
if (reference == null || reference.get() == null) {
return;
Span clientSpan = ref.getAndSet(null);
if (clientSpan == null) {
return; // Unexpected. In the handle method, without a span to finish!
}
handler().handleReceive(new WrappedHttpClientResponse(httpClientResponse),
throwable, (Span) reference.get());
WrappedHttpClientResponse response = resp != null
? new WrappedHttpClientResponse(resp) : null;
handler().handleReceive(response, error, clientSpan);
}
}
@@ -260,12 +270,12 @@ class HttpClientBeanPostProcessor implements BeanPostProcessor {
@Override
public String path() {
return delegate.path();
return "/" + delegate.path(); // TODO: reactor/reactor-netty#999
}
@Override
public String url() {
return delegate.uri();
return delegate.resourceUrl();
}
@Override
@@ -288,6 +298,11 @@ class HttpClientBeanPostProcessor implements BeanPostProcessor {
this.delegate = delegate;
}
@Override
public String method() {
return delegate.method().name();
}
@Override
public Object unwrap() {
return delegate;

View File

@@ -162,8 +162,8 @@ public class TraceWebClientAutoConfiguration {
@Bean
static HttpClientBeanPostProcessor httpClientBeanPostProcessor(
BeanFactory beanFactory) {
return new HttpClientBeanPostProcessor(beanFactory);
ConfigurableApplicationContext springContext) {
return new HttpClientBeanPostProcessor(springContext);
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.sleuth.instrument.web.client;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -131,9 +132,14 @@ final class TraceExchangeFilterFunction implements ExchangeFilterFunction {
}
};
private static final String CLIENT_SPAN_KEY = "sleuth.webclient.clientSpan";
static final String CLIENT_SPAN_KEY = "sleuth.webclient.clientSpan";
private static final String CANCELLED_SUBSCRIPTION_ERROR = "CANCELLED";
static final Exception CANCELLED_ERROR = new CancellationException("CANCELLED") {
@Override
public Throwable fillInStackTrace() {
return this; // stack trace doesn't add value here
}
};
final ConfigurableApplicationContext springContext;
@@ -170,6 +176,8 @@ final class TraceExchangeFilterFunction implements ExchangeFilterFunction {
}
MonoWebClientTrace trace = new MonoWebClientTrace(next, wrapper.buildRequest(),
this, span);
// TODO: investigate why this commit leaks a scope:
// 8f5bcdabd7af23df443e771432eb85597f3b3076
tracer().withSpanInScope(parentSpan);
return trace;
}
@@ -356,13 +364,14 @@ final class TraceExchangeFilterFunction implements ExchangeFilterFunction {
return this.context;
}
void handleReceive(Span clientSpan, ClientResponse clientResponse,
Throwable throwable) {
void handleReceive(Span clientSpan, @Nullable ClientResponse res,
@Nullable Throwable error) {
if (log.isTraceEnabled()) {
log.trace("Handling receive");
}
this.handler.handleReceive(new HttpClientResponse(clientResponse),
throwable, clientSpan);
HttpClientResponse response = res != null ? new HttpClientResponse(res)
: null;
this.handler.handleReceive(response, error, clientSpan);
if (log.isTraceEnabled()) {
log.trace("Closed scope");
}
@@ -374,32 +383,31 @@ final class TraceExchangeFilterFunction implements ExchangeFilterFunction {
+ this.span + "]");
}
this.span.tag("error", CANCELLED_SUBSCRIPTION_ERROR);
handleReceive(this.span, null, null);
handleReceive(this.span, null, CANCELLED_ERROR);
}
void terminateSpan(@Nullable ClientResponse clientResponse,
@Nullable Throwable throwable) {
@Nullable Throwable error) {
if (clientResponse == null) {
if (log.isDebugEnabled()) {
log.debug("No response was returned. Will close the span ["
+ this.span + "]");
}
handleReceive(this.span, clientResponse, throwable);
handleReceive(this.span, null, error);
return;
}
int statusCode = clientResponse.rawStatusCode();
boolean error = statusCode >= 400;
if (error) {
boolean isHttpError = statusCode >= 400;
if (isHttpError) {
if (log.isDebugEnabled()) {
log.debug(
"Non positive status code was returned from the call. Will close the span ["
+ this.span + "]");
}
throwable = new RestClientException(
error = new RestClientException(
"Status code of the response is [" + statusCode + "]");
}
handleReceive(this.span, clientResponse, throwable);
handleReceive(this.span, clientResponse, error);
}
}

View File

@@ -99,9 +99,13 @@ final class TracingFeignClient implements Client {
Throwable error = null;
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) {
Response res = this.delegate.execute(request.build(), options);
if (res != null) { // possibly null on bad implementation or mocks
if (res != null) {
response = new HttpClientResponse(res);
}
else { // possibly null on bad implementation or mocks
response = new HttpClientResponse(
Response.builder().request(req).build());
}
return res;
}
catch (IOException | RuntimeException | Error e) {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.reactor;
package org.springframework.cloud.sleuth.internal;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -25,8 +25,16 @@ import org.springframework.lang.Nullable;
/**
* Avoids calling the expensive {@link ConfigurableApplicationContext#getBean(Class)} many
* times or throwing an exception.
*
* <p>
* Note: This is an internal class to sleuth and must not be used by external code.
*/
final class LazyBean<T> {
public final class LazyBean<T> {
public static <T> LazyBean<T> create(ConfigurableApplicationContext springContext,
Class<T> requiredType) {
return new LazyBean<>(springContext, requiredType);
}
// spring-jcl uses commons-logging, so do we.
private static final Log log = LogFactory.getLog(LazyBean.class);
@@ -47,7 +55,7 @@ final class LazyBean<T> {
* @return the bean value or null if there was an exception getting it.
*/
@Nullable
T get() {
public T get() {
if (this.value != null) {
return this.value;
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright 2013-2019 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
*
* https://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.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import brave.propagation.B3SinglePropagation;
import brave.propagation.CurrentTraceContext;
import brave.propagation.CurrentTraceContext.Scope;
import brave.propagation.Propagation;
import brave.propagation.TraceContext;
import brave.sampler.Sampler;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.client.HttpClient;
import reactor.netty.http.client.HttpClientResponse;
import reactor.netty.http.client.PrematureCloseException;
import reactor.netty.http.server.HttpServer;
import zipkin2.Span;
import zipkin2.reporter.Reporter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* This tests {@link HttpClient} instrumentation performed by
* {@link HttpClientBeanPostProcessor}, as wired by auto-configuration.
*
* <p>
* <em>Note:</em> {@link HttpClient} can be an implementation of {@link WebClient}, so
* care should be taken to also test that integration. For example, it would be easy to
* create duplicate client spans for the same request.
*/
@SpringBootTest(classes = ReactorNettyHttpClientSpringBootTests.TestConfiguration.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE)
@RunWith(SpringRunner.class)
public class ReactorNettyHttpClientSpringBootTests {
DisposableServer disposableServer;
@Autowired
HttpClient httpClient;
@Autowired
BlockingQueue<Span> spans;
@Autowired
CurrentTraceContext currentTraceContext;
TraceContext context = TraceContext.newBuilder().traceId(1).spanId(1).sampled(true)
.build();
@After
public void tearDown() {
if (disposableServer != null) {
disposableServer.disposeNow();
}
this.spans.clear();
}
@Test
public void shouldRecordRemoteEndpoint() throws Exception {
disposableServer = HttpServer.create().port(0)
.handle((in, out) -> out.sendString(Flux.just("foo"))).bindNow();
HttpClientResponse response = httpClient.port(disposableServer.port()).get()
.uri("/").response().block();
assertThat(response.status()).isEqualTo(HttpResponseStatus.OK);
Span clientSpan = takeClientSpan();
assertThat(clientSpan.remoteEndpoint()).satisfiesAnyOf(
ep -> assertThat(ep.ipv4()).isNotNull(),
ep -> assertThat(ep.ipv6()).isNotNull());
assertThat(clientSpan.remoteEndpoint().portAsInt()).isNotZero();
}
@Test
public void shouldUseInvocationContext() throws Exception {
disposableServer = HttpServer.create().port(0)
// this reads the trace context header, b3, returning it in the response
.handle((in, out) -> out
.sendString(Flux.just(in.requestHeaders().get("b3"))))
.bindNow();
String b3SingleHeaderReadByServer;
try (Scope ws = currentTraceContext.newScope(context)) {
b3SingleHeaderReadByServer = httpClient.port(disposableServer.port()).get()
.uri("/").responseContent().aggregate().asString().block();
}
Span clientSpan = takeClientSpan();
assertThat(b3SingleHeaderReadByServer).isEqualTo(context.traceIdString() + "-"
+ clientSpan.id() + "-1-" + context.spanIdString());
}
@Test
public void shouldSendTraceContextToServer_rootSpan() throws Exception {
disposableServer = HttpServer.create().port(0)
// this reads the trace context header, b3, returning it in the response
.handle((in, out) -> out
.sendString(Flux.just(in.requestHeaders().get("b3"))))
.bindNow();
Mono<String> request = httpClient.port(disposableServer.port()).get().uri("/")
.responseContent().aggregate().asString();
String b3SingleHeaderReadByServer = request.block();
Span clientSpan = takeClientSpan();
assertThat(b3SingleHeaderReadByServer)
.isEqualTo(clientSpan.traceId() + "-" + clientSpan.id() + "-1");
}
@Test
public void shouldTagOnRequestError() throws InterruptedException {
disposableServer = HttpServer.create().port(0).handle((req, resp) -> {
throw new RuntimeException("test");
}).bindNow();
Mono<String> request = httpClient.port(disposableServer.port()).get().uri("/")
.responseContent().aggregate().asString();
assertThatThrownBy(request::block)
.hasCauseInstanceOf(PrematureCloseException.class);
Span clientSpan = takeClientSpan();
assertThat(clientSpan.tags()).containsKey("error");
}
/** Call this to block until a span was reported */
Span takeClientSpan() throws InterruptedException {
Span result = spans.poll(1, TimeUnit.SECONDS);
assertThat(result).withFailMessage("Span was not reported").isNotNull();
assertThat(result.kind()).isEqualTo(Span.Kind.CLIENT);
return result;
}
@Configuration
@EnableAutoConfiguration
static class TestConfiguration {
@Bean
Propagation.Factory propagationFactory() {
return B3SinglePropagation.FACTORY;
}
@Bean
Sampler sampler() {
return Sampler.ALWAYS_SAMPLE;
}
/**
* Use a blocking queue as it is simpler than wrapping everything in awaitility
*/
@Bean
BlockingQueue<Span> spans() {
return new LinkedBlockingQueue<>();
}
@Bean
Reporter<zipkin2.Span> spanReporter(BlockingQueue<Span> spans) {
return spans::add;
}
@Bean
HttpClient reactorHttpClient() {
return HttpClient.create();
}
}
}

View File

@@ -17,8 +17,9 @@
package org.springframework.cloud.sleuth.instrument.web.client.feign;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import brave.Span;
import brave.Tracer;
@@ -36,9 +37,7 @@ import org.mockito.BDDMockito;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.sleuth.instrument.web.SleuthHttpParserAccessor;
import org.springframework.cloud.sleuth.util.ArrayListSpanReporter;
import static org.assertj.core.api.BDDAssertions.then;
@@ -48,15 +47,16 @@ import static org.assertj.core.api.BDDAssertions.then;
@RunWith(MockitoJUnitRunner.class)
public class TracingFeignClientTests {
ArrayListSpanReporter reporter = new ArrayListSpanReporter();
Request request = Request.create("GET", "https://foo", new HashMap<>(), null, null);
@Mock
BeanFactory beanFactory;
Request.Options options = new Request.Options();
List<zipkin2.Span> spans = new ArrayList<>();
Tracing tracing = Tracing.newBuilder()
.currentTraceContext(ThreadLocalCurrentTraceContext.newBuilder()
.addScopeDecorator(StrictScopeDecorator.create()).build())
.spanReporter(this.reporter).build();
.spanReporter(spans::add).build();
Tracer tracer = this.tracing.tracer();
@@ -78,17 +78,13 @@ public class TracingFeignClientTests {
Span span = this.tracer.nextSpan().name("foo");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span.start())) {
this.traceFeignClient
.execute(
Request.create("GET", "https://foo", new HashMap<>(),
"".getBytes(), Charset.defaultCharset()),
new Request.Options());
this.traceFeignClient.execute(this.request, this.options);
}
finally {
span.finish();
}
then(this.reporter.getSpans().get(0)).extracting("kind.ordinal")
then(spans.get(0)).extracting("kind.ordinal")
.isEqualTo(Span.Kind.CLIENT.ordinal());
}
@@ -99,11 +95,7 @@ public class TracingFeignClientTests {
.willThrow(new RuntimeException("exception has occurred"));
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span.start())) {
this.traceFeignClient
.execute(
Request.create("GET", "https://foo", new HashMap<>(),
"".getBytes(), Charset.defaultCharset()),
new Request.Options());
this.traceFeignClient.execute(this.request, this.options);
BDDAssertions.fail("Exception should have been thrown");
}
catch (Exception e) {
@@ -112,21 +104,17 @@ public class TracingFeignClientTests {
span.finish();
}
then(this.reporter.getSpans().get(0)).extracting("kind.ordinal")
then(this.spans.get(0)).extracting("kind.ordinal")
.isEqualTo(Span.Kind.CLIENT.ordinal());
then(this.reporter.getSpans().get(0).tags()).containsEntry("error",
"exception has occurred");
then(this.spans.get(0).tags()).containsEntry("error", "exception has occurred");
}
@Test
public void should_shorten_the_span_name() throws IOException {
this.traceFeignClient
.execute(
Request.create("GET", "https://foo/" + bigName(), new HashMap<>(),
"".getBytes(), Charset.defaultCharset()),
new Request.Options());
this.traceFeignClient.execute(Request.create("GET", "https://foo/" + bigName(),
new HashMap<>(), null, null), this.options);
then(this.reporter.getSpans().get(0).name()).hasSize(50);
then(this.spans.get(0).name()).hasSize(50);
}
private String bigName() {

View File

@@ -36,6 +36,7 @@ import brave.propagation.TraceContextOrSamplingFlags;
import brave.sampler.Sampler;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
@@ -51,8 +52,8 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import reactor.netty.http.client.HttpClient;
import reactor.netty.http.client.HttpClientResponse;
import org.reactivestreams.Subscription;
import reactor.core.publisher.BaseSubscriber;
import zipkin2.Annotation;
import zipkin2.reporter.Reporter;
@@ -112,8 +113,7 @@ public class WebClientTests {
static final String SAMPLED_NAME = "X-B3-Sampled";
static final String PARENT_ID_NAME = "X-B3-ParentSpanId";
private static final org.apache.commons.logging.Log log = LogFactory
.getLog(WebClientTests.class);
private static final Log log = LogFactory.getLog(WebClientTests.class);
@Rule
public final SpringMethodRule springMethodRule = new SpringMethodRule();
@@ -134,9 +134,6 @@ public class WebClientTests {
@Autowired
HttpClientBuilder httpClientBuilder; // #845
@Autowired
HttpClient nettyHttpClient;
@Autowired
HttpAsyncClientBuilder httpAsyncClientBuilder; // #845
@@ -276,30 +273,6 @@ public class WebClientTests {
then(this.reporter.getSpans()).isNotEmpty();
}
@Test
@SuppressWarnings("unchecked")
public void shouldAttachTraceIdWhenCallingAnotherServiceForNettyHttpClient()
throws Exception {
Span span = this.tracer.nextSpan().name("foo").start();
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) {
HttpClientResponse response = this.nettyHttpClient.get()
.uri("http://localhost:" + this.port).response().block();
then(response).isNotNull();
}
Awaitility.await().untilAsserted(() -> {
then(this.tracer.currentSpan()).isNull();
System.out.println("Collected span " + this.reporter.getSpans());
then(this.reporter.getSpans()).isNotEmpty()
.extracting("traceId", String.class)
// we can have some bizarre spans popping up
.contains(span.context().traceIdString());
then(this.reporter.getSpans()).extracting("kind.name").contains("CLIENT");
});
}
@Test
@SuppressWarnings("unchecked")
public void shouldAttachTraceIdWhenCallingAnotherServiceForHttpClient()
@@ -378,7 +351,7 @@ public class WebClientTests {
@Test
@SuppressWarnings("unchecked")
public void shouldWorkWhenCustomStatusCodeIsReturned() throws InterruptedException {
public void shouldWorkWhenCustomStatusCodeIsReturned() {
Span span = this.tracer.nextSpan().name("foo").start();
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) {
@@ -397,6 +370,21 @@ public class WebClientTests {
.contains("CLIENT");
}
@Test
public void shouldTagOnCancel() {
this.webClient.get().uri("http://localhost:" + this.port + "/doNotSkip")
.retrieve().bodyToMono(String.class)
.subscribe(new BaseSubscriber<String>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
cancel();
}
});
then(this.reporter.getSpans()).isNotEmpty();
then(this.reporter.getSpans().get(0).tags()).containsEntry("error", "CANCELLED");
}
@Test
public void shouldRespectSkipPattern() {
this.webClient.get().uri("http://localhost:" + this.port + "/skip").retrieve()
@@ -599,11 +587,6 @@ public class WebClientTests {
return new MyRestTemplateCustomizer();
}
@Bean
HttpClient reactorHttpClient() {
return HttpClient.create();
}
}
static class MyRestTemplateCustomizer implements RestTemplateCustomizer {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.reactor;
package org.springframework.cloud.sleuth.internal;
import brave.propagation.CurrentTraceContext;
import org.junit.Test;

View File

@@ -31,7 +31,7 @@
<name>spring-cloud-sleuth-dependencies</name>
<description>Spring Cloud Sleuth Dependencies</description>
<properties>
<brave.version>5.9.3</brave.version>
<brave.version>5.9.5</brave.version>
<brave.opentracing.version>0.35.1</brave.opentracing.version>
<grpc.spring.boot.version>3.4.1</grpc.spring.boot.version>
</properties>

View File

@@ -1,166 +0,0 @@
/*
* Copyright 2013-2019 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
*
* https://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.zipkin2;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import zipkin2.Span;
import zipkin2.codec.BytesEncoder;
import zipkin2.reporter.AsyncReporter;
import zipkin2.reporter.InMemoryReporterMetrics;
import zipkin2.reporter.Reporter;
import zipkin2.reporter.ReporterMetrics;
import zipkin2.reporter.Sender;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.Assert;
/**
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
* Auto-configuration} that will provide backwards compatibility to be able to support
* multiple tracing systems on the classpath.
*
* Needs to be auto-configured before {@link ZipkinAutoConfiguration} in order to create a
* {@link Reporter span reporter} if needed.
*
* @author Tim Ysewyn
* @since 2.1.0
* @see ZipkinAutoConfiguration
* @deprecated left for backward compatibility
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = { "spring.sleuth.enabled", "spring.zipkin.enabled" },
matchIfMissing = true)
@AutoConfigureBefore(ZipkinAutoConfiguration.class)
@Deprecated
public class ZipkinBackwardsCompatibilityAutoConfiguration {
/**
* Reporter that is depending on a {@link Sender} bean which is created in another
* auto-configuration than {@link ZipkinAutoConfiguration}.
* @param reporterMetrics metrics
* @param zipkin zipkin properties
* @param spanBytesEncoder encoder
* @param beanFactory Spring's Bean Factory
* @return span reporter
* @deprecated left for backwards compatibility
*/
@Bean
@Conditional(BackwardsCompatibilityCondition.class)
@Deprecated
Reporter<Span> reporter(ReporterMetrics reporterMetrics, ZipkinProperties zipkin,
BytesEncoder<Span> spanBytesEncoder, DefaultListableBeanFactory beanFactory) {
List<String> beanNames = new ArrayList<>(
Arrays.asList(beanFactory.getBeanNamesForType(Sender.class)));
beanNames.remove(ZipkinAutoConfiguration.SENDER_BEAN_NAME);
Sender sender = (Sender) beanFactory.getBean(beanNames.get(0));
// historical constraint. Note: AsyncReporter supports memory bounds
return AsyncReporter.builder(sender).queuedMaxSpans(1000)
.messageTimeout(zipkin.getMessageTimeout(), TimeUnit.SECONDS)
.metrics(reporterMetrics).build(spanBytesEncoder);
}
/**
* Only used for creating a reporter bean with the method above.
* @param zipkinProperties zipkin properties
* @return bytes encoder
* @deprecated left for backwards compatibility
*/
@Bean
@ConditionalOnMissingBean
@Deprecated
BytesEncoder<Span> spanBytesEncoder(ZipkinProperties zipkinProperties) {
return zipkinProperties.getEncoder();
}
/**
* Deprecated because this is moved to {@link TraceAutoConfiguration}. Left for
* backwards compatibility reasons.
* @return reporter metrics
* @deprecated left for backwards compatibility
*/
@Bean
@ConditionalOnMissingBean
@Deprecated
ReporterMetrics zipkinReporterMetrics() {
return new InMemoryReporterMetrics();
}
/**
* Old approach: - one sender - one reporter
*
* This auto configuration verifies if we have the old approach. In which case we
* define the missing beans.
*
* In case of having 0 or more than 1 sender and there is a reporter, we don't need to
* use the backward compatibility bean setup.
*/
static class BackwardsCompatibilityCondition extends SpringBootCondition
implements ConfigurationCondition {
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
Assert.isInstanceOf(DefaultListableBeanFactory.class,
context.getBeanFactory());
DefaultListableBeanFactory listableBeanFactory = (DefaultListableBeanFactory) context
.getBeanFactory();
int foundSenders = listableBeanFactory
.getBeanNamesForType(Sender.class).length;
// Previously we supported 1 Sender bean at a time
// which could be overridden by another auto-configuration.
// Now we support both the overridden bean and our default zipkinSender bean.
// Since this config is adapting the old config we're searching for exactly 1
// `Sender` bean before `ZipkinAutoConfiguration` kicks in.
if (foundSenders != 1) {
return ConditionOutcome.noMatch(
"None or multiple Sender beans found - no reason to apply backwards compatibility");
}
int foundReporters = listableBeanFactory
.getBeanNamesForType(Reporter.class).length;
// Check if we need to provide a Reporter bean for the overridden Sender bean
if (foundReporters > 0) {
return ConditionOutcome.noMatch(
"The old config setup already defines its own Reporter bean");
}
return ConditionOutcome.match();
}
}
}

View File

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

View File

@@ -1,72 +0,0 @@
/*
* Copyright 2013-2019 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
*
* https://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.zipkin2;
import org.junit.Test;
import zipkin2.codec.BytesEncoder;
import zipkin2.reporter.Reporter;
import zipkin2.reporter.ReporterMetrics;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Tim Ysewyn
*/
public class ZipkinBackwardsCompatibilityAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
ZipkinBackwardsCompatibilityAutoConfiguration.class,
ZipkinAutoConfiguration.class, TraceAutoConfiguration.class));
@Test
public void shouldLoadBeans() {
this.contextRunner.run(context -> {
assertThat(context.getBean(ZipkinProperties.class)).isNotNull();
assertThat(context.getBean(Reporter.class)).isNotNull();
assertThat(context.getBean(BytesEncoder.class)).isNotNull();
assertThat(context.getBean(ReporterMetrics.class)).isNotNull();
});
}
@Test
public void shouldNotLoadBackwardsCompatibilityConfigWhenZipkinDisabled() {
this.contextRunner.withPropertyValues("spring.zipkin.enabled=false")
.run(context -> {
assertThat(context.getBeansOfType(ZipkinProperties.class)).isEmpty();
assertThat(context.getBeansOfType(BytesEncoder.class)).isEmpty();
assertThat(context.getBean(ReporterMetrics.class)).isNotNull(); // TraceAutoConfiguration
assertThat(context.getBean(Reporter.class)).isNotNull(); // noOpSpanReporter
});
}
@Test
public void shouldNotLoadBackwardsCompatibilityConfigWhenSleuthDisabled() {
this.contextRunner.withPropertyValues("spring.sleuth.enabled=false")
.run(context -> {
assertThat(context.getBeansOfType(ZipkinProperties.class)).isEmpty();
assertThat(context.getBeansOfType(BytesEncoder.class)).isEmpty();
assertThat(context.getBeansOfType(ReporterMetrics.class)).isEmpty();
assertThat(context.getBeansOfType(Reporter.class)).isEmpty();
});
}
}