Exposes TraceExchangeFilterFunction; fixes gh-1847

This commit is contained in:
Marcin Grzejszczak
2021-03-16 16:16:15 +01:00
parent 8f9c51ac81
commit e48ebb045e
2 changed files with 386 additions and 335 deletions

View File

@@ -0,0 +1,386 @@
/*
* Copyright 2013-2021 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.Collection;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Subscription;
import reactor.core.CoreSubscriber;
import reactor.core.Scannable;
import reactor.core.publisher.Mono;
import reactor.util.annotation.Nullable;
import reactor.util.context.Context;
import org.springframework.cloud.sleuth.CurrentTraceContext;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.TraceContext;
import org.springframework.cloud.sleuth.http.HttpClientHandler;
import org.springframework.cloud.sleuth.http.HttpClientRequest;
import org.springframework.cloud.sleuth.http.HttpClientResponse;
import org.springframework.cloud.sleuth.instrument.reactor.TraceContextPropagator;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
/**
* Trace representation of {@link ExchangeFilterFunction}.
*
* @author Marcin Grzejszczak
* @since 3.0.2
*/
public final class TraceExchangeFilterFunction implements ExchangeFilterFunction {
private static final Log log = LogFactory.getLog(TraceExchangeFilterFunction.class);
final ConfigurableApplicationContext springContext;
// Lazy initialized fields
HttpClientHandler handler;
CurrentTraceContext currentTraceContext;
TraceExchangeFilterFunction(ConfigurableApplicationContext springContext) {
this.springContext = springContext;
}
public static ExchangeFilterFunction create(ConfigurableApplicationContext springContext) {
return new TraceExchangeFilterFunction(springContext);
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return new MonoWebClientTrace(next, request, this);
}
CurrentTraceContext currentTraceContext() {
if (this.currentTraceContext == null) {
this.currentTraceContext = this.springContext.getBean(CurrentTraceContext.class);
}
return this.currentTraceContext;
}
HttpClientHandler handler() {
if (this.handler == null) {
this.handler = this.springContext.getBean(HttpClientHandler.class);
}
return this.handler;
}
private static final class MonoWebClientTrace extends Mono<ClientResponse>
implements Scannable, TraceContextPropagator {
final ExchangeFunction next;
final ClientRequest request;
final HttpClientHandler handler;
final CurrentTraceContext currentTraceContext;
MonoWebClientTrace(ExchangeFunction next, ClientRequest request, TraceExchangeFilterFunction filterFunction) {
this.next = next;
this.request = request;
this.handler = filterFunction.handler();
this.currentTraceContext = filterFunction.currentTraceContext();
}
@Override
public void subscribe(CoreSubscriber<? super ClientResponse> subscriber) {
Context context = subscriber.currentContext();
if (log.isTraceEnabled()) {
log.trace("Got the following context [" + context + "]");
}
ClientRequestWrapper wrapper = new ClientRequestWrapper(this.request);
TraceContext parent = context.hasKey(TraceContext.class) ? context.get(TraceContext.class) : null;
Span span = handler.handleSend(wrapper, parent);
if (log.isTraceEnabled()) {
log.trace("HttpClientHandler::handleSend: " + span);
}
// NOTE: We are starting the client span for the request here, but it could be
// canceled prior to actually being invoked. TraceWebClientSubscription will
// abandon this span, if cancel() happens before request().
this.next.exchange(wrapper.buildRequest())
.subscribe(new TraceWebClientSubscriber(subscriber, context, span, parent, this));
}
@Nullable
@Override
public Object scanUnsafe(Attr key) {
if (key == Attr.RUN_STYLE) {
return Attr.RunStyle.SYNC;
}
return null;
}
}
/**
* Subscriber for WebClient.
*/
static final class TraceWebClientSubscriber extends AtomicReference<Span>
implements CoreSubscriber<ClientResponse>, Scannable {
final CoreSubscriber<? super ClientResponse> actual;
final Context context;
@Nullable
final TraceContext parent;
final HttpClientHandler handler;
final CurrentTraceContext currentTraceContext;
TraceWebClientSubscriber(CoreSubscriber<? super ClientResponse> actual, Context ctx, Span clientSpan,
TraceContext parent, MonoWebClientTrace mono) {
this.actual = actual;
this.parent = parent;
this.handler = mono.handler;
this.currentTraceContext = mono.currentTraceContext;
this.context = this.parent != null && !this.parent.equals(ctx.getOrDefault(TraceContext.class, null))
? ctx.put(TraceContext.class, this.parent) : ctx;
set(clientSpan);
}
@Override
public void onSubscribe(Subscription subscription) {
this.actual.onSubscribe(new TraceWebClientSubscription(subscription, this));
}
@Override
public void onNext(ClientResponse response) {
try (CurrentTraceContext.Scope scope = this.currentTraceContext.maybeScope(parent)) {
if (log.isTraceEnabled()) {
log.trace("OnNext");
}
// decorate response body
this.actual.onNext(response);
}
finally {
Span span = getAndSet(null);
if (span != null) {
if (log.isTraceEnabled()) {
log.trace("OnNext finally");
}
// TODO: is there a way to read the request at response time?
this.handler.handleReceive(new ClientResponseWrapper(response), span);
}
}
}
@Override
public void onError(Throwable t) {
try (CurrentTraceContext.Scope scope = this.currentTraceContext.maybeScope(parent)) {
if (log.isTraceEnabled()) {
log.trace("OnError");
}
this.actual.onError(t);
}
finally {
Span span = getAndSet(null);
if (span != null) {
if (log.isTraceEnabled()) {
log.trace("OnError finally");
}
span.error(t);
span.end();
}
}
}
@Override
public void onComplete() {
try (CurrentTraceContext.Scope scope = this.currentTraceContext.maybeScope(parent)) {
if (log.isTraceEnabled()) {
log.trace("OnComplete");
}
this.actual.onComplete();
}
finally {
Span span = getAndSet(null);
if (span != null) {
// TODO: backfill empty test:
// https://github.com/spring-cloud/spring-cloud-sleuth/issues/1570
if (log.isTraceEnabled()) {
log.trace("Reached OnComplete without finishing [" + span + "]");
}
span.abandon();
}
}
}
@Override
public Context currentContext() {
return this.context;
}
@Override
public Object scanUnsafe(Attr key) {
if (key == Attr.RUN_STYLE) {
return Attr.RunStyle.SYNC;
}
return null;
}
}
static class TraceWebClientSubscription implements Subscription {
static final Exception CANCELLED_ERROR = new CancellationException("CANCELLED") {
@Override
public Throwable fillInStackTrace() {
return this; // stack trace doesn't add value here
}
};
final AtomicReference<Span> pendingSpan;
final Subscription delegate;
volatile boolean requested;
TraceWebClientSubscription(Subscription delegate, AtomicReference<Span> pendingSpan) {
this.delegate = delegate;
this.pendingSpan = pendingSpan;
}
@Override
public void request(long n) {
requested = true;
delegate.request(n); // Not scoping to save overhead
}
@Override
public void cancel() {
delegate.cancel(); // Not scoping to save overhead
// Check to see if Subscription.cancel() happened after request(),
// but before another signal (like onComplete) completed the span.
Span span = pendingSpan.getAndSet(null);
if (span != null) {
if (log.isTraceEnabled()) {
log.trace("Subscription was cancelled. TraceWebClientBeanPostProcessor Will close the span [" + span
+ "]");
}
if (!requested) { // Abandon the span.
span.abandon();
}
else { // Request was canceled in-flight
span.error(CANCELLED_ERROR);
span.end();
}
}
}
}
private static final class ClientRequestWrapper implements HttpClientRequest {
final ClientRequest delegate;
final ClientRequest.Builder builder;
ClientRequestWrapper(ClientRequest delegate) {
this.delegate = delegate;
this.builder = ClientRequest.from(delegate);
}
@Override
public Collection<String> headerNames() {
return this.delegate.headers().keySet();
}
@Override
public Object unwrap() {
return delegate;
}
@Override
public String method() {
return delegate.method().name();
}
@Override
public String path() {
return delegate.url().getPath();
}
@Override
public String url() {
return delegate.url().toString();
}
@Override
public String header(String name) {
return delegate.headers().getFirst(name);
}
@Override
public void header(String name, String value) {
builder.header(name, value);
}
ClientRequest buildRequest() {
return builder.build();
}
}
static final class ClientResponseWrapper implements HttpClientResponse {
final ClientResponse delegate;
ClientResponseWrapper(ClientResponse delegate) {
this.delegate = delegate;
}
@Override
public Collection<String> headerNames() {
return this.delegate.headers().asHttpHeaders().keySet();
}
@Override
public Object unwrap() {
return delegate;
}
@Override
public int statusCode() {
// unlike statusCode(), this doesn't throw
return Math.max(delegate.rawStatusCode(), 0);
}
@Override
public String header(String header) {
List<String> headers = delegate.headers().header(header);
if (headers.isEmpty()) {
return null;
}
return headers.get(0);
}
}
}

View File

@@ -103,338 +103,3 @@ public class TraceWebClientBeanPostProcessor implements BeanPostProcessor {
}
final class TraceExchangeFilterFunction implements ExchangeFilterFunction {
private static final Log log = LogFactory.getLog(TraceExchangeFilterFunction.class);
final ConfigurableApplicationContext springContext;
// Lazy initialized fields
HttpClientHandler handler;
CurrentTraceContext currentTraceContext;
TraceExchangeFilterFunction(ConfigurableApplicationContext springContext) {
this.springContext = springContext;
}
public static ExchangeFilterFunction create(ConfigurableApplicationContext springContext) {
return new TraceExchangeFilterFunction(springContext);
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return new MonoWebClientTrace(next, request, this);
}
CurrentTraceContext currentTraceContext() {
if (this.currentTraceContext == null) {
this.currentTraceContext = this.springContext.getBean(CurrentTraceContext.class);
}
return this.currentTraceContext;
}
HttpClientHandler handler() {
if (this.handler == null) {
this.handler = this.springContext.getBean(HttpClientHandler.class);
}
return this.handler;
}
private static final class MonoWebClientTrace extends Mono<ClientResponse>
implements Scannable, TraceContextPropagator {
final ExchangeFunction next;
final ClientRequest request;
final HttpClientHandler handler;
final CurrentTraceContext currentTraceContext;
MonoWebClientTrace(ExchangeFunction next, ClientRequest request, TraceExchangeFilterFunction filterFunction) {
this.next = next;
this.request = request;
this.handler = filterFunction.handler();
this.currentTraceContext = filterFunction.currentTraceContext();
}
@Override
public void subscribe(CoreSubscriber<? super ClientResponse> subscriber) {
Context context = subscriber.currentContext();
if (log.isTraceEnabled()) {
log.trace("Got the following context [" + context + "]");
}
ClientRequestWrapper wrapper = new ClientRequestWrapper(this.request);
TraceContext parent = context.hasKey(TraceContext.class) ? context.get(TraceContext.class) : null;
Span span = handler.handleSend(wrapper, parent);
if (log.isTraceEnabled()) {
log.trace("HttpClientHandler::handleSend: " + span);
}
// NOTE: We are starting the client span for the request here, but it could be
// canceled prior to actually being invoked. TraceWebClientSubscription will
// abandon this span, if cancel() happens before request().
this.next.exchange(wrapper.buildRequest())
.subscribe(new TraceWebClientSubscriber(subscriber, context, span, parent, this));
}
@Nullable
@Override
public Object scanUnsafe(Scannable.Attr key) {
if (key == Scannable.Attr.RUN_STYLE) {
return Scannable.Attr.RunStyle.SYNC;
}
return null;
}
}
/**
* Subscriber for WebClient.
*/
static final class TraceWebClientSubscriber extends AtomicReference<Span>
implements CoreSubscriber<ClientResponse>, Scannable {
final CoreSubscriber<? super ClientResponse> actual;
final Context context;
@Nullable
final TraceContext parent;
final HttpClientHandler handler;
final CurrentTraceContext currentTraceContext;
TraceWebClientSubscriber(CoreSubscriber<? super ClientResponse> actual, Context ctx, Span clientSpan,
TraceContext parent, MonoWebClientTrace mono) {
this.actual = actual;
this.parent = parent;
this.handler = mono.handler;
this.currentTraceContext = mono.currentTraceContext;
this.context = this.parent != null && !this.parent.equals(ctx.getOrDefault(TraceContext.class, null))
? ctx.put(TraceContext.class, this.parent) : ctx;
set(clientSpan);
}
@Override
public void onSubscribe(Subscription subscription) {
this.actual.onSubscribe(new TraceWebClientSubscription(subscription, this));
}
@Override
public void onNext(ClientResponse response) {
try (CurrentTraceContext.Scope scope = this.currentTraceContext.maybeScope(parent)) {
if (log.isTraceEnabled()) {
log.trace("OnNext");
}
// decorate response body
this.actual.onNext(response);
}
finally {
Span span = getAndSet(null);
if (span != null) {
if (log.isTraceEnabled()) {
log.trace("OnNext finally");
}
// TODO: is there a way to read the request at response time?
this.handler.handleReceive(new ClientResponseWrapper(response), span);
}
}
}
@Override
public void onError(Throwable t) {
try (CurrentTraceContext.Scope scope = this.currentTraceContext.maybeScope(parent)) {
if (log.isTraceEnabled()) {
log.trace("OnError");
}
this.actual.onError(t);
}
finally {
Span span = getAndSet(null);
if (span != null) {
if (log.isTraceEnabled()) {
log.trace("OnError finally");
}
span.error(t);
span.end();
}
}
}
@Override
public void onComplete() {
try (CurrentTraceContext.Scope scope = this.currentTraceContext.maybeScope(parent)) {
if (log.isTraceEnabled()) {
log.trace("OnComplete");
}
this.actual.onComplete();
}
finally {
Span span = getAndSet(null);
if (span != null) {
// TODO: backfill empty test:
// https://github.com/spring-cloud/spring-cloud-sleuth/issues/1570
if (log.isTraceEnabled()) {
log.trace("Reached OnComplete without finishing [" + span + "]");
}
span.abandon();
}
}
}
@Override
public Context currentContext() {
return this.context;
}
@Override
public Object scanUnsafe(Scannable.Attr key) {
if (key == Scannable.Attr.RUN_STYLE) {
return Scannable.Attr.RunStyle.SYNC;
}
return null;
}
}
static class TraceWebClientSubscription implements Subscription {
static final Exception CANCELLED_ERROR = new CancellationException("CANCELLED") {
@Override
public Throwable fillInStackTrace() {
return this; // stack trace doesn't add value here
}
};
final AtomicReference<Span> pendingSpan;
final Subscription delegate;
volatile boolean requested;
TraceWebClientSubscription(Subscription delegate, AtomicReference<Span> pendingSpan) {
this.delegate = delegate;
this.pendingSpan = pendingSpan;
}
@Override
public void request(long n) {
requested = true;
delegate.request(n); // Not scoping to save overhead
}
@Override
public void cancel() {
delegate.cancel(); // Not scoping to save overhead
// Check to see if Subscription.cancel() happened after request(),
// but before another signal (like onComplete) completed the span.
Span span = pendingSpan.getAndSet(null);
if (span != null) {
if (log.isTraceEnabled()) {
log.trace("Subscription was cancelled. TraceWebClientBeanPostProcessor Will close the span [" + span
+ "]");
}
if (!requested) { // Abandon the span.
span.abandon();
}
else { // Request was canceled in-flight
span.error(CANCELLED_ERROR);
span.end();
}
}
}
}
private static final class ClientRequestWrapper implements HttpClientRequest {
final ClientRequest delegate;
final ClientRequest.Builder builder;
ClientRequestWrapper(ClientRequest delegate) {
this.delegate = delegate;
this.builder = ClientRequest.from(delegate);
}
@Override
public Collection<String> headerNames() {
return this.delegate.headers().keySet();
}
@Override
public Object unwrap() {
return delegate;
}
@Override
public String method() {
return delegate.method().name();
}
@Override
public String path() {
return delegate.url().getPath();
}
@Override
public String url() {
return delegate.url().toString();
}
@Override
public String header(String name) {
return delegate.headers().getFirst(name);
}
@Override
public void header(String name, String value) {
builder.header(name, value);
}
ClientRequest buildRequest() {
return builder.build();
}
}
static final class ClientResponseWrapper implements HttpClientResponse {
final ClientResponse delegate;
ClientResponseWrapper(ClientResponse delegate) {
this.delegate = delegate;
}
@Override
public Collection<String> headerNames() {
return this.delegate.headers().asHttpHeaders().keySet();
}
@Override
public Object unwrap() {
return delegate;
}
@Override
public int statusCode() {
// unlike statusCode(), this doesn't throw
return Math.max(delegate.rawStatusCode(), 0);
}
@Override
public String header(String header) {
List<String> headers = delegate.headers().header(header);
if (headers.isEmpty()) {
return null;
}
return headers.get(0);
}
}
}