com.nike.wingtips.apache.httpclient.WingtipsHttpClientBuilder Maven / Gradle / Ivy
package com.nike.wingtips.apache.httpclient;
import com.nike.internal.util.StringUtils;
import com.nike.wingtips.Span;
import com.nike.wingtips.Span.SpanPurpose;
import com.nike.wingtips.Tracer;
import com.nike.wingtips.apache.httpclient.tag.ApacheHttpClientTagAdapter;
import com.nike.wingtips.apache.httpclient.util.WingtipsApacheHttpClientUtil;
import com.nike.wingtips.tags.HttpTagAndSpanNamingAdapter;
import com.nike.wingtips.tags.HttpTagAndSpanNamingStrategy;
import com.nike.wingtips.tags.NoOpHttpTagAdapter;
import com.nike.wingtips.tags.NoOpHttpTagStrategy;
import com.nike.wingtips.tags.ZipkinHttpTagStrategy;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpExecutionAware;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.execchain.ClientExecChain;
import org.apache.http.protocol.HttpProcessor;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import static com.nike.wingtips.apache.httpclient.util.WingtipsApacheHttpClientUtil.propagateTracingHeaders;
/**
* (NOTE: This class is strongly recommended instead of {@link WingtipsApacheHttpClientInterceptor} if you have
* control over which {@link HttpClientBuilder} is used to create your {@link HttpClient}s. Reasons for this are
* described at the bottom of this class javadoc.)
*
* This class is an extension of {@link HttpClientBuilder}, where any {@link HttpClient}s that you {@link #build()}
* will propagate Wingtips tracing on the {@link HttpClient} request headers and optionally surround the request in
* a subspan.
*
*
If the subspan option is enabled but there's no current span on the current thread when a request executes,
* then a new root span (new trace) will be created rather than a subspan. In either case the newly created span will
* have a {@link Span#getSpanPurpose()} of {@link Span.SpanPurpose#CLIENT} since the subspan is for a client call.
* The {@link Span#getSpanName()} for the newly created span will be generated by {@link
* #getSubspanSpanName(HttpRequest, HttpTagAndSpanNamingStrategy, HttpTagAndSpanNamingAdapter)}. Instantiate this
* class with a custom {@link HttpTagAndSpanNamingStrategy} and/or {@link HttpTagAndSpanNamingAdapter} (preferred),
* or override that method (last resort) if you want a different span naming format.
*
*
Note that if you have the subspan option turned off then the {@link Tracer#getCurrentSpan()}'s tracing info
* will be propagated downstream if it's available, but if no current span exists on the current thread when the
* request is executed then no tracing logic will occur as there's no tracing info to propagate. Turning on the
* subspan option mitigates this as it guarantees there will be a span to propagate.
*
*
As mentioned at the top of this class' javadocs, this class is the preferred way to automatically handle Wingtips
* tracing propagation and subspans for {@link HttpClient} requests if you have control over the {@link
* HttpClientBuilder} that gets used to generate {@link HttpClient}s. The other option is interceptors via {@link
* WingtipsApacheHttpClientInterceptor} (when you don't have control over the {@link HttpClientBuilder}), however this
* class is preferred instead of the interceptor where possible for the following reasons:
*
* -
* There are certain types of exceptions that can occur that prevent the response interceptor side of {@link
* WingtipsApacheHttpClientInterceptor} from executing, thus preventing the subspan around the request from
* completing. This is a consequence of how the Apache HttpClient interceptors work. The only way to guarantee
* subspan completion is to use this class instead of the interceptor.
*
* -
* There are several ways for interceptors to be accidentally wiped out, e.g. {@link
* HttpClientBuilder#setHttpProcessor(HttpProcessor)}.
*
* -
* This class makes sure that any subspan *fully* surrounds the request, including all other interceptors that
* are executed.
*
* -
* When using the interceptor instead of this class you have to remember to add the interceptor as both a
* request interceptor ({@link HttpRequestInterceptor}) *and* response interceptor ({@link
* HttpResponseInterceptor}) on the {@link HttpClientBuilder} you use, or tracing will be broken.
*
*
* That said, the interceptors do work perfectly well when you aren't able to use this builder class as long as they
* are setup correctly *and* you never experience any of the exceptions that cause the response interceptor to be
* ignored (this is usually impossible to guarantee, making it a major issue for most use cases - again, please use
* this class instead of the interceptor if you can).
*
* @author Nic Munroe
*/
@SuppressWarnings("WeakerAccess")
public class WingtipsHttpClientBuilder extends HttpClientBuilder {
protected boolean surroundCallsWithSubspan;
protected HttpTagAndSpanNamingStrategy tagAndNamingStrategy;
protected HttpTagAndSpanNamingAdapter tagAndNamingAdapter;
/**
* Creates a new instance with the subspan option turned on and the default {@link HttpTagAndSpanNamingStrategy}
* and {@link HttpTagAndSpanNamingAdapter} ({@link ZipkinHttpTagStrategy} and {@link ApacheHttpClientTagAdapter}).
*/
public WingtipsHttpClientBuilder() {
this(true);
}
/**
* Creates a new instance with the subspan option set to the value of the {@code surroundCallsWithSubspan}
* argument, and the default {@link HttpTagAndSpanNamingStrategy} and {@link HttpTagAndSpanNamingAdapter}
* ({@link ZipkinHttpTagStrategy} and {@link ApacheHttpClientTagAdapter}).
*
* @param surroundCallsWithSubspan Pass in true to have requests surrounded in a subspan, false to disable the
* subspan option.
*/
public WingtipsHttpClientBuilder(boolean surroundCallsWithSubspan) {
this(
surroundCallsWithSubspan,
ZipkinHttpTagStrategy.getDefaultInstance(),
ApacheHttpClientTagAdapter.getDefaultInstance()
);
}
/**
* Creates a new instance with the subspan option set to the value of the {@code surroundCallsWithSubspan}
* argument, and the given {@link HttpTagAndSpanNamingStrategy} and {@link HttpTagAndSpanNamingAdapter}.
*
* @param surroundCallsWithSubspan Pass in true to have requests surrounded in a subspan, false to disable the
* subspan option.
* @param tagAndNamingStrategy The span tag and naming strategy to use - cannot be null. If you really want no
* tag and naming strategy, then pass in {@link NoOpHttpTagStrategy#getDefaultInstance()}.
* @param tagAndNamingAdapter The tag and naming adapter to use - cannot be null. If you really want no tag and
* naming adapter, then pass in {@link NoOpHttpTagAdapter#getDefaultInstance()}.
*/
public WingtipsHttpClientBuilder(
boolean surroundCallsWithSubspan,
HttpTagAndSpanNamingStrategy tagAndNamingStrategy,
HttpTagAndSpanNamingAdapter tagAndNamingAdapter
) {
if (tagAndNamingStrategy == null) {
throw new IllegalArgumentException(
"tagAndNamingStrategy cannot be null - if you really want no strategy, use NoOpHttpTagStrategy"
);
}
if (tagAndNamingAdapter == null) {
throw new IllegalArgumentException(
"tagAndNamingAdapter cannot be null - if you really want no adapter, use NoOpHttpTagAdapter"
);
}
this.surroundCallsWithSubspan = surroundCallsWithSubspan;
this.tagAndNamingStrategy = tagAndNamingStrategy;
this.tagAndNamingAdapter = tagAndNamingAdapter;
}
/**
* @return Static factory method for creating a new {@link WingtipsHttpClientBuilder} instance with the subspan
* option turned on and the default {@link HttpTagAndSpanNamingStrategy} and {@link HttpTagAndSpanNamingAdapter}
* ({@link ZipkinHttpTagStrategy} and {@link ApacheHttpClientTagAdapter}).
*/
public static WingtipsHttpClientBuilder create() {
return new WingtipsHttpClientBuilder();
}
/**
* @param surroundCallsWithSubspan Pass in true to have requests surrounded in a subspan, false to disable the
* subspan option.
* @return Static factory method for creating a new {@link WingtipsHttpClientBuilder} instance with the subspan
* option set to the value of the {@code surroundCallsWithSubspan} argument, and the default
* {@link HttpTagAndSpanNamingStrategy} and {@link HttpTagAndSpanNamingAdapter}
* ({@link ZipkinHttpTagStrategy} and {@link ApacheHttpClientTagAdapter}).
*/
public static WingtipsHttpClientBuilder create(boolean surroundCallsWithSubspan) {
return new WingtipsHttpClientBuilder(surroundCallsWithSubspan);
}
/**
* @param surroundCallsWithSubspan Pass in true to have requests surrounded in a subspan, false to disable the
* subspan option.
* @param tagAndNamingStrategy The span tag and naming strategy to use - cannot be null. If you really want no
* tag and naming strategy, then pass in {@link NoOpHttpTagStrategy#getDefaultInstance()}.
* @param tagAndNamingAdapter The tag and naming adapter to use - cannot be null. If you really want no tag and
* naming adapter, then pass in {@link NoOpHttpTagAdapter#getDefaultInstance()}.
* @return Static factory method for creating a new {@link WingtipsHttpClientBuilder} instance with the subspan
* option set to the value of the {@code surroundCallsWithSubspan} argument, and the given
* {@link HttpTagAndSpanNamingStrategy} and {@link HttpTagAndSpanNamingAdapter}.
*/
public static WingtipsHttpClientBuilder create(
boolean surroundCallsWithSubspan,
HttpTagAndSpanNamingStrategy tagAndNamingStrategy,
HttpTagAndSpanNamingAdapter tagAndNamingAdapter
) {
return new WingtipsHttpClientBuilder(surroundCallsWithSubspan, tagAndNamingStrategy, tagAndNamingAdapter);
}
@Override
protected ClientExecChain decorateProtocolExec(final ClientExecChain protocolExec) {
final boolean myHttpClientSurroundCallsWithSubspan = surroundCallsWithSubspan;
return new ClientExecChain() {
@Override
@SuppressWarnings("TryFinallyCanBeTryWithResources")
public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request,
HttpClientContext clientContext,
HttpExecutionAware execAware) throws IOException, HttpException {
if(myHttpClientSurroundCallsWithSubspan) {
return createNewSubSpanAndExecute(route, request, clientContext, execAware);
}
return propagateHeadersAndExecute(route, request, clientContext, execAware);
}
protected CloseableHttpResponse createNewSubSpanAndExecute(HttpRoute route, HttpRequestWrapper request,
HttpClientContext clientContext,
HttpExecutionAware execAware) throws IOException, HttpException {
// Will start a new trace if necessary, or a subspan if a trace is already in progress.
Span spanAroundCall = Tracer.getInstance().startSpanInCurrentContext(
getSubspanSpanName(request, tagAndNamingStrategy, tagAndNamingAdapter), SpanPurpose.CLIENT
);
CloseableHttpResponse response = null;
Throwable errorForTagging = null;
try {
tagAndNamingStrategy.handleRequestTagging(spanAroundCall, request, tagAndNamingAdapter);
response = propagateHeadersAndExecute(route, request, clientContext, execAware);
return response;
} catch(Throwable t) {
errorForTagging = t;
throw t;
}
finally {
try {
// Handle response/error tagging and final span name.
tagAndNamingStrategy.handleResponseTaggingAndFinalSpanName(
spanAroundCall, request, response, errorForTagging, tagAndNamingAdapter
);
}
finally {
// Span.close() contains the logic we want - if the spanAroundCall was an overall span (new
// trace) then tracer.completeRequestSpan() will be called, otherwise it's a subspan and
// tracer.completeSubSpan() will be called.
spanAroundCall.close();
}
}
}
protected CloseableHttpResponse propagateHeadersAndExecute(
HttpRoute route,
HttpRequestWrapper request,
HttpClientContext clientContext,
HttpExecutionAware execAware
) throws IOException, HttpException {
propagateTracingHeaders(request, Tracer.getInstance().getCurrentSpan());
return protocolExec.execute(route, request, clientContext, execAware);
}
};
}
/**
* Returns the name that should be used for the subspan surrounding the call. Defaults to whatever {@link
* HttpTagAndSpanNamingStrategy#getInitialSpanName(Object, HttpTagAndSpanNamingAdapter)} returns, with a fallback
* of {@link WingtipsApacheHttpClientUtil#getFallbackSubspanSpanName(HttpRequest)} if the naming strategy returned
* null or blank string. You can override this method to return something else if you want different behavior and
* you don't want to adjust the naming strategy or adapter.
*
* @param request The request that is about to be executed.
* @param namingStrategy The {@link HttpTagAndSpanNamingStrategy} being used.
* @param adapter The {@link HttpTagAndSpanNamingAdapter} being used.
* @return The name that should be used for the subspan surrounding the call.
*/
protected @NotNull String getSubspanSpanName(
@NotNull HttpRequest request,
@NotNull HttpTagAndSpanNamingStrategy namingStrategy,
@NotNull HttpTagAndSpanNamingAdapter adapter
) {
// Try the naming strategy first.
String subspanNameFromStrategy = namingStrategy.getInitialSpanName(request, adapter);
if (StringUtils.isNotBlank(subspanNameFromStrategy)) {
return subspanNameFromStrategy;
}
// The naming strategy didn't have anything for us. Fall back to something reasonable.
return WingtipsApacheHttpClientUtil.getFallbackSubspanSpanName(request);
}
/**
* @return The current value of the subspan option.
*/
public boolean isSurroundCallsWithSubspan() {
return surroundCallsWithSubspan;
}
/**
* Sets the builder's subspan option value. New {@link HttpClient}s generated with {@link #build()} will use this
* value when processing requests, but setting this here will not affect any {@link HttpClient}s that have already
* been built - it only affects future-generated {@link HttpClient}s
*
* @param surroundCallsWithSubspan Pass in true to have requests surrounded in a subspan, false to disable the
* subspan option.
* @return This builder after setting the subspan option to the desired value.
*/
public WingtipsHttpClientBuilder setSurroundCallsWithSubspan(boolean surroundCallsWithSubspan) {
this.surroundCallsWithSubspan = surroundCallsWithSubspan;
return this;
}
}