spanNamingAndTaggingStrategy = DefaultAsyncHttpClientHelperSpanNamingAndTaggingStrategy.getDefaultInstance();
+
+ public Builder() {
+ }
+
+ /**
+ * Sets the default {@link SignatureCalculator} that will used when making requests with the configured
+ * {@link AsyncHttpClient}.
+ *
+ * {@link SignatureCalculator} is a class that is normally used to set a header based on the full request being sent.
+ * It provides a hook as the last step before sending the request.
+ */
+ public AsyncHttpClientHelper.Builder setDefaultSignatureCalculator(
+ SignatureCalculator signatureCalculator) {
+ this.defaultSignatureCalculator = signatureCalculator;
+ return this;
+ }
+
+ /**
+ * Sets the flag to determine if SubSpan are created around the downstream calls
+ */
+ public AsyncHttpClientHelper.Builder setPerformSubSpanAroundDownstreamCalls(boolean performSubSpanAroundDownstreamCalls) {
+ this.performSubSpanAroundDownstreamCalls = performSubSpanAroundDownstreamCalls;
+ return this;
+ }
+
+ /**
+ * Sets the {@link SpanNamingAndTaggingStrategy} that should be used when this class surrounds outbound calls
+ * with subspans (i.e. when {@link #performSubSpanAroundDownstreamCalls} is true). The standard/default
+ * class that is used is {@link DefaultAsyncHttpClientHelperSpanNamingAndTaggingStrategy} - if you want to
+ * adjust something, you will probably want to start with that class as a base.
+ *
+ * @param spanNamingAndTaggingStrategy The strategy to use.
+ * @return This same instance being called, to enable fluent setup.
+ */
+ public AsyncHttpClientHelper.Builder setSpanNamingAndTaggingStrategy(
+ SpanNamingAndTaggingStrategy spanNamingAndTaggingStrategy
+ ) {
+ if (spanNamingAndTaggingStrategy == null) {
+ throw new IllegalArgumentException("spanNamingAndTaggingStrategy cannot be null");
+ }
+
+ this.spanNamingAndTaggingStrategy = spanNamingAndTaggingStrategy;
+ return this;
+ }
+
+ public AsyncHttpClientHelper.Builder setMaxRequestRetry(int maxRequestRetry) {
+ this.clientConfigBuilder.setMaxRequestRetry(maxRequestRetry);
+ return this;
+ }
+
+ public AsyncHttpClientHelper.Builder setRequestTimeout(int requestTimeout) {
+ this.clientConfigBuilder.setRequestTimeout(requestTimeout);
+ return this;
+ }
+
+ public AsyncHttpClientHelper.Builder setConnectionTtl(int connectionTtl) {
+ this.clientConfigBuilder.setConnectionTtl(connectionTtl);
+ return this;
+ }
+
+ public AsyncHttpClientHelper.Builder setClientConfigBuilder(DefaultAsyncHttpClientConfig.Builder clientConfigBuilder) {
+ if (clientConfigBuilder == null) {
+ throw new IllegalArgumentException("clientConfigBuilder cannot be null");
+ }
+
+ this.clientConfigBuilder = clientConfigBuilder;
+ return this;
+ }
+
+ public AsyncHttpClientHelper.Builder setNameResolver(NameResolver nameResolver) {
+ this.nameResolver = nameResolver;
+ return this;
+ }
+
+ public AsyncHttpClientHelper build() {
+ return new AsyncHttpClientHelper(this);
+ }
+ }
+
+ /**
+ * Call this before one of the {@code executeAsyncHttpRequest(...)} methods in order to get a request builder you
+ * can populate with query params, headers, body, etc. If you want to specify a custom circuit breaker (or disable
+ * circuit breaking entirely) for this call then use {@link #getRequestBuilder(String, HttpMethod, Optional,
+ * boolean)} instead. This method tells the HTTP client to use a default circuit breaker based on the host being
+ * called.
+ */
+ public RequestBuilderWrapper getRequestBuilder(String url, HttpMethod method) {
+ return getRequestBuilder(url, method, Optional.empty(), false);
+ }
+
+ /**
+ * Call this before one of the {@code executeAsyncHttpRequest(...)} methods in order to get a request builder you
+ * can populate with query params, headers, body, etc. Pass in a non-empty {@code customCircuitBreaker} argument to
+ * specify the exact circuit breaker you want to use, pass in an empty {@code customCircuitBreaker} if you want the
+ * HTTP client to use a default one based on the host being called, and pass in true for the {@code
+ * disableCircuitBreaker} argument if you want to disable circuit breaking entirely for this call.
+ */
+ public RequestBuilderWrapper getRequestBuilder(String url, HttpMethod method,
+ Optional> customCircuitBreaker,
+ boolean disableCircuitBreaker) {
+ RequestBuilderWrapper wrapper = generateRequestBuilderWrapper(
+ url, method, customCircuitBreaker, disableCircuitBreaker
+ );
+
+ // By default, The AsyncHttpClient doesn't properly split traffic when a DNS has multiple IP addresses associated with it,
+ // so we have to set a name resolver that does.
+ wrapper.requestBuilder.setNameResolver(nameResolver);
+
+ return wrapper;
+ }
+
+ protected RequestBuilderWrapper generateRequestBuilderWrapper(String url, HttpMethod method,
+ Optional> customCircuitBreaker,
+ boolean disableCircuitBreaker) {
+ String httpMethod = method.name();
+ switch (httpMethod) {
+ case "CONNECT":
+ return new RequestBuilderWrapper(url, httpMethod, asyncHttpClient.prepareConnect(url),
+ customCircuitBreaker, disableCircuitBreaker);
+ case "DELETE":
+ return new RequestBuilderWrapper(url, httpMethod, asyncHttpClient.prepareDelete(url),
+ customCircuitBreaker, disableCircuitBreaker);
+ case "GET":
+ return new RequestBuilderWrapper(url, httpMethod, asyncHttpClient.prepareGet(url),
+ customCircuitBreaker,
+ disableCircuitBreaker);
+ case "HEAD":
+ return new RequestBuilderWrapper(url, httpMethod, asyncHttpClient.prepareHead(url),
+ customCircuitBreaker, disableCircuitBreaker);
+ case "POST":
+ return new RequestBuilderWrapper(url, httpMethod, asyncHttpClient.preparePost(url),
+ customCircuitBreaker, disableCircuitBreaker);
+ case "OPTIONS":
+ return new RequestBuilderWrapper(url, httpMethod, asyncHttpClient.prepareOptions(url),
+ customCircuitBreaker, disableCircuitBreaker);
+ case "PUT":
+ return new RequestBuilderWrapper(url, httpMethod, asyncHttpClient.preparePut(url),
+ customCircuitBreaker,
+ disableCircuitBreaker);
+ case "PATCH":
+ return new RequestBuilderWrapper(url, httpMethod, asyncHttpClient.preparePatch(url),
+ customCircuitBreaker, disableCircuitBreaker);
+ case "TRACE":
+ return new RequestBuilderWrapper(url, httpMethod, asyncHttpClient.prepareTrace(url),
+ customCircuitBreaker, disableCircuitBreaker);
+ default:
+ logger.warn(
+ "The given method {} is not directly supported. We will try to force it anyway. The returned request builder may or may not work.",
+ httpMethod);
+ return new RequestBuilderWrapper(url, httpMethod,
+ asyncHttpClient.preparePost(url).setMethod(httpMethod),
+ customCircuitBreaker,
+ disableCircuitBreaker);
+ }
+ }
+
+ /**
+ * Executes the given request asynchronously, handling the response with the given responseHandlerFunction, and
+ * returns a {@link CompletableFuture} that represents the result of executing the
+ * responseHandlerFunction on the downstream response. Any error anywhere along the way will cause the returned
+ * future to be completed with {@link CompletableFuture#completeExceptionally(Throwable)}.
+ *
+ * NOTE: This is a helper method for calling {@link #executeAsyncHttpRequest(RequestBuilderWrapper,
+ * AsyncResponseHandler, java.util.Deque, java.util.Map)} that uses the current thread's {@link
+ * Tracer#getCurrentSpanStackCopy()} and {@link org.slf4j.MDC#getCopyOfContextMap()} for the for the distributed
+ * trace stack and MDC info for the downstream call.
+ */
+ public CompletableFuture executeAsyncHttpRequest(
+ RequestBuilderWrapper requestBuilderWrapper,
+ AsyncResponseHandler responseHandlerFunction) {
+ Map mdcContextMap = MDC.getCopyOfContextMap();
+ Deque distributedTraceStack = Tracer.getInstance().getCurrentSpanStackCopy();
+
+ return executeAsyncHttpRequest(requestBuilderWrapper, responseHandlerFunction,
+ distributedTraceStack,
+ mdcContextMap);
+ }
+
+ /**
+ * Executes the given request asynchronously, handling the response with the given responseHandlerFunction, and
+ * returns a {@link CompletableFuture} that represents the result of executing the
+ * responseHandlerFunction on the downstream response. Any error anywhere along the way will cause the returned
+ * future to be completed with {@link CompletableFuture#completeExceptionally(Throwable)}.
+ *
+ * NOTE: This is a helper method for calling {@link #executeAsyncHttpRequest(RequestBuilderWrapper,
+ * AsyncResponseHandler, java.util.Deque, java.util.Map)} that uses {@link
+ * ChannelAttributes#getHttpProcessingStateForChannel(ChannelHandlerContext)} to extract the {@link
+ * HttpProcessingState} from the given ctx argument, and then grabs {@link
+ * HttpProcessingState#getDistributedTraceStack()} and {@link HttpProcessingState#getLoggerMdcContextMap()} to use
+ * as the distributed trace stack and MDC info for the downstream call.
+ */
+ public CompletableFuture executeAsyncHttpRequest(
+ RequestBuilderWrapper requestBuilderWrapper,
+ AsyncResponseHandler responseHandlerFunction,
+ ChannelHandlerContext ctx) {
+
+ HttpProcessingState state = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get();
+ if (state == null)
+ throw new IllegalStateException("state cannot be null");
+
+ Map mdcContextMap = state.getLoggerMdcContextMap();
+ Deque distributedTraceStack = state.getDistributedTraceStack();
+
+ requestBuilderWrapper.setCtx(ctx);
+
+ return executeAsyncHttpRequest(requestBuilderWrapper, responseHandlerFunction,
+ distributedTraceStack,
+ mdcContextMap);
+ }
+
+ /**
+ * Executes the given request asynchronously, handling the response with the given responseHandlerFunction, and
+ * returns a {@link CompletableFuture} that represents the result of executing the
+ * responseHandlerFunction on the downstream response. Any error anywhere along the way will cause the returned
+ * future to be completed with {@link CompletableFuture#completeExceptionally(Throwable)}.
+ *
+ * Distributed Tracing and MDC for the downstream call: The given {@code distributedTraceStackForCall} and
+ * {@code mdcContextForCall} arguments are used to setup distributed trace and MDC info for the downstream call so
+ * that the callback will be performed with that data attached to whatever thread the callback is done on.
+ */
+ public CompletableFuture executeAsyncHttpRequest(
+ RequestBuilderWrapper requestBuilderWrapper,
+ AsyncResponseHandler responseHandlerFunction,
+ Deque distributedTraceStackForCall,
+ Map mdcContextForCall) {
+ CompletableFuture completableFutureResponse = new CompletableFuture<>();
+
+ try {
+ Optional> circuitBreakerManualTask =
+ getCircuitBreaker(requestBuilderWrapper).map(CircuitBreaker::newManualModeTask);
+
+ // If we have a circuit breaker, give it a chance to throw an exception if the circuit is open/tripped
+ circuitBreakerManualTask.ifPresent(ManualModeTask::throwExceptionIfCircuitBreakerIsOpen);
+
+ // Setup the async completion handler for the call.
+ AsyncCompletionHandlerWithTracingAndMdcSupport asyncCompletionHandler =
+ new AsyncCompletionHandlerWithTracingAndMdcSupport<>(
+ completableFutureResponse, responseHandlerFunction,
+ performSubSpanAroundDownstreamCalls,
+ requestBuilderWrapper, circuitBreakerManualTask, distributedTraceStackForCall,
+ mdcContextForCall,
+ spanNamingAndTaggingStrategy
+ );
+
+ // Add distributed trace headers to the downstream call if we have a span.
+ Span spanForCall = asyncCompletionHandler.getSpanForCall();
+ if (spanForCall != null) {
+ HttpRequestTracingUtils.propagateTracingHeaders(
+ (headerKey, headerValue) -> {
+ if (headerValue != null) {
+ requestBuilderWrapper.requestBuilder.setHeader(headerKey, headerValue);
+ }
+ },
+ spanForCall
+ );
+ }
+
+ // Add span tags if we're doing a subspan around the call.
+ if (performSubSpanAroundDownstreamCalls && spanForCall != null) {
+ spanNamingAndTaggingStrategy.handleRequestTagging(spanForCall, requestBuilderWrapper);
+ }
+
+ // Execute the downstream call. The completableFutureResponse will be completed or completed exceptionally
+ // depending on the result of the call.
+ requestBuilderWrapper.requestBuilder.execute(asyncCompletionHandler);
+ } catch (Throwable t) {
+ // Log the error for later debugging, unless it's a CircuitBreakerOpenException, which is expected and
+ // normal when the circuit breaker associated with this request has been tripped.
+ if (!(t instanceof CircuitBreakerOpenException)) {
+ logger.error(
+ "An error occurred while trying to set up an async HTTP call for method {} and URL {}. "
+ + "The CompletableFuture will be instantly failed with this error",
+ requestBuilderWrapper.httpMethod, requestBuilderWrapper.url, t
+ );
+ }
+ completableFutureResponse.completeExceptionally(t);
+ }
+
+ return completableFutureResponse;
+ }
+
+ protected Optional> getCircuitBreaker(
+ RequestBuilderWrapper requestBuilderWrapper) {
+ if (requestBuilderWrapper.disableCircuitBreaker)
+ return Optional.empty();
+
+ // Circuit breaking is enabled for this call. So we return the custom one specified or use the default one if a
+ // custom one is not specified.
+ if (requestBuilderWrapper.customCircuitBreaker.isPresent())
+ return requestBuilderWrapper.customCircuitBreaker;
+
+ // No custom circuit breaker. Use the default for the given request's host.
+ Uri uri = Uri.create(requestBuilderWrapper.url);
+ String host = uri.getHost();
+ EventLoop nettyEventLoop = requestBuilderWrapper.getCtx() == null
+ ? null
+ : requestBuilderWrapper.getCtx().channel().eventLoop();
+ CircuitBreaker defaultStatusCodeCircuitBreaker = getDefaultHttpStatusCodeCircuitBreakerForKey(
+ host, Optional.ofNullable(nettyEventLoop), Optional.ofNullable(nettyEventLoop)
+ );
+ return Optional.of(
+ new CircuitBreakerDelegate<>(
+ defaultStatusCodeCircuitBreaker,
+ response -> (response == null ? null : response.getStatusCode())
+ )
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/AsyncHttpClientHelperTagAdapter.java b/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/AsyncHttpClientHelperTagAdapter.java
new file mode 100644
index 00000000..0a3e5770
--- /dev/null
+++ b/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/AsyncHttpClientHelperTagAdapter.java
@@ -0,0 +1,141 @@
+package com.nike.riposte.client.asynchttp;
+
+import com.nike.internal.util.StringUtils;
+import com.nike.riposte.util.HttpUtils;
+import com.nike.wingtips.tags.HttpTagAndSpanNamingAdapter;
+
+import org.asynchttpclient.Response;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * Extension of {@link com.nike.wingtips.tags.HttpTagAndSpanNamingAdapter} that knows how to handle the request
+ * and response for {@link AsyncHttpClientHelper} ({@link RequestBuilderWrapper} and {@link Response}).
+ *
+ * @author Nic Munroe
+ */
+public class AsyncHttpClientHelperTagAdapter extends HttpTagAndSpanNamingAdapter {
+
+ @SuppressWarnings("WeakerAccess")
+ protected static final AsyncHttpClientHelperTagAdapter
+ DEFAULT_INSTANCE = new AsyncHttpClientHelperTagAdapter();
+
+ /**
+ * @return A reusable, thread-safe, singleton instance of this class that can be used by anybody who wants to use
+ * this class and does not need any customization.
+ */
+ @SuppressWarnings("unchecked")
+ public static AsyncHttpClientHelperTagAdapter getDefaultInstance() {
+ return DEFAULT_INSTANCE;
+ }
+
+ @Nullable
+ @Override
+ public String getRequestUrl(@Nullable RequestBuilderWrapper request) {
+ if (request == null) {
+ return null;
+ }
+
+ return request.getUrl();
+ }
+
+ @Nullable
+ @Override
+ public String getRequestPath(@Nullable RequestBuilderWrapper request) {
+ if (request == null) {
+ return null;
+ }
+
+ String result = request.getUrl();
+ if (StringUtils.isBlank(result)) {
+ return null;
+ }
+
+ // Chop out the query string (if any).
+ result = HttpUtils.extractPath(result);
+
+ // If it starts with '/' then there's nothing left for us to do - it's already the path.
+ if (result.startsWith("/")) {
+ return result;
+ }
+
+ // Doesn't start with '/'. We expect it to start with http at this point.
+ if (!result.toLowerCase().startsWith("http")) {
+ // Didn't start with http. Not sure what to do with this at this point, so return null.
+ return null;
+ }
+
+ // It starts with http. Chop out the scheme and host/port.
+ int schemeColonAndDoubleSlashIndex = result.indexOf("://");
+ if (schemeColonAndDoubleSlashIndex < 0) {
+ // It didn't have a colon-double-slash after the scheme. Not sure what to do at this point, so return null.
+ return null;
+ }
+
+ int firstSlashIndexAfterSchemeDoubleSlash = result.indexOf('/', (schemeColonAndDoubleSlashIndex + 3));
+ if (firstSlashIndexAfterSchemeDoubleSlash < 0) {
+ // No other slashes after the scheme colon-double-slash, so no real path. The path at this point is
+ // effectively "/".
+ return "/";
+ }
+
+ return result.substring(firstSlashIndexAfterSchemeDoubleSlash);
+ }
+
+ @Nullable
+ @Override
+ public String getRequestUriPathTemplate(@Nullable RequestBuilderWrapper request, @Nullable Response response) {
+ // Nothing we can do by default - this needs to be overridden on a per-project basis and given some smarts
+ // based on project-specific knowledge.
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Integer getResponseHttpStatus(@Nullable Response response) {
+ if (response == null) {
+ return null;
+ }
+
+ return response.getStatusCode();
+ }
+
+ @Nullable
+ @Override
+ public String getRequestHttpMethod(@Nullable RequestBuilderWrapper request) {
+ if (request == null) {
+ return null;
+ }
+
+ return request.getHttpMethod();
+ }
+
+ @Nullable
+ @Override
+ public String getHeaderSingleValue(
+ @Nullable RequestBuilderWrapper request, @NotNull String headerKey
+ ) {
+ // There's no way for us to get the headers - they're hidden away in the request.requestBuilder object and
+ // we have no access to them.
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public List getHeaderMultipleValue(@Nullable RequestBuilderWrapper request, @NotNull String headerKey) {
+ // There's no way for us to get the headers - they're hidden away in the request.requestBuilder object and
+ // we have no access to them.
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getSpanHandlerTagValue(
+ @Nullable RequestBuilderWrapper request, @Nullable Response response
+ ) {
+ return "riposte.asynchttpclienthelper";
+ }
+}
diff --git a/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/AsyncResponseHandler.java b/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/AsyncResponseHandler.java
new file mode 100644
index 00000000..34845ec6
--- /dev/null
+++ b/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/AsyncResponseHandler.java
@@ -0,0 +1,17 @@
+package com.nike.riposte.client.asynchttp;
+
+import org.asynchttpclient.Response;
+
+/**
+ * Interface representing a handler for an async downstream HTTP call's response. Used by {@link AsyncHttpClientHelper}.
+ *
+ * @author Nic Munroe
+ */
+public interface AsyncResponseHandler {
+
+ /**
+ * @return The result of handling the given response.
+ */
+ T handleResponse(Response response) throws Throwable;
+
+}
diff --git a/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/DefaultAsyncHttpClientHelperSpanNamingAndTaggingStrategy.java b/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/DefaultAsyncHttpClientHelperSpanNamingAndTaggingStrategy.java
new file mode 100644
index 00000000..a0aff244
--- /dev/null
+++ b/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/DefaultAsyncHttpClientHelperSpanNamingAndTaggingStrategy.java
@@ -0,0 +1,114 @@
+package com.nike.riposte.client.asynchttp;
+
+import com.nike.riposte.server.config.distributedtracing.SpanNamingAndTaggingStrategy;
+import com.nike.wingtips.Span;
+import com.nike.wingtips.SpanMutator;
+import com.nike.wingtips.tags.HttpTagAndSpanNamingAdapter;
+import com.nike.wingtips.tags.HttpTagAndSpanNamingStrategy;
+import com.nike.wingtips.tags.ZipkinHttpTagStrategy;
+
+import org.asynchttpclient.Response;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * A concrete implementation of {@link SpanNamingAndTaggingStrategy} for {@link AsyncHttpClientHelper} that works with
+ * {@link RequestBuilderWrapper} requests, Ning {@link Response}s, and Wingtips {@link Span}s. This class
+ * delegates the actual work to Wingtips {@link HttpTagAndSpanNamingStrategy} and {@link
+ * HttpTagAndSpanNamingAdapter} classes.
+ *
+ * By default (default constructor, or {@link #getDefaultInstance()}) you'll get {@link
+ * ZipkinHttpTagStrategy#getDefaultInstance()} for the Wingtips strategy and {@link
+ * AsyncHttpClientHelperTagAdapter#getDefaultInstance()} for the adapter.
+ *
+ *
You can use the alternate constructor if you want different implementations, e.g. you could pass a custom {@link
+ * AsyncHttpClientHelperTagAdapter} that overrides {@link HttpTagAndSpanNamingAdapter#getInitialSpanName(Object)}
+ * and/or {@link HttpTagAndSpanNamingAdapter#getFinalSpanName(Object, Object)} if you want to adjust the span
+ * names that are generated.
+ *
+ * @author Nic Munroe
+ */
+public class DefaultAsyncHttpClientHelperSpanNamingAndTaggingStrategy
+ extends SpanNamingAndTaggingStrategy {
+
+ protected final @NotNull HttpTagAndSpanNamingStrategy tagAndNamingStrategy;
+ protected final @NotNull HttpTagAndSpanNamingAdapter tagAndNamingAdapter;
+
+ protected static final DefaultAsyncHttpClientHelperSpanNamingAndTaggingStrategy DEFAULT_INSTANCE =
+ new DefaultAsyncHttpClientHelperSpanNamingAndTaggingStrategy();
+
+ /**
+ * @return A reusable, thread-safe, singleton instance of this class that can be used by anybody who wants to use
+ * this class and does not need any customization.
+ */
+ public static DefaultAsyncHttpClientHelperSpanNamingAndTaggingStrategy getDefaultInstance() {
+ return DEFAULT_INSTANCE;
+ }
+
+ /**
+ * Creates a new instance that uses {@link ZipkinHttpTagStrategy#getDefaultInstance()} and {@link
+ * AsyncHttpClientHelperTagAdapter#getDefaultInstance()} to do the work of span naming and tagging.
+ */
+ public DefaultAsyncHttpClientHelperSpanNamingAndTaggingStrategy() {
+ this(ZipkinHttpTagStrategy.getDefaultInstance(), AsyncHttpClientHelperTagAdapter.getDefaultInstance());
+ }
+
+ /**
+ * Creates a new instance that uses the given arguments to do the work of span naming and tagging.
+ *
+ * @param tagAndNamingStrategy The {@link HttpTagAndSpanNamingStrategy} to use.
+ * @param tagAndNamingAdapter The {@link HttpTagAndSpanNamingAdapter} to use.
+ */
+ @SuppressWarnings("ConstantConditions")
+ public DefaultAsyncHttpClientHelperSpanNamingAndTaggingStrategy(
+ @NotNull HttpTagAndSpanNamingStrategy tagAndNamingStrategy,
+ @NotNull 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.tagAndNamingStrategy = tagAndNamingStrategy;
+ this.tagAndNamingAdapter = tagAndNamingAdapter;
+ }
+
+ @Override
+ public @Nullable String doGetInitialSpanName(
+ @NotNull RequestBuilderWrapper request
+ ) {
+ return tagAndNamingStrategy.getInitialSpanName(request, tagAndNamingAdapter);
+ }
+
+ @Override
+ public void doChangeSpanName(@NotNull Span span, @NotNull String newName) {
+ SpanMutator.changeSpanName(span, newName);
+ }
+
+ @Override
+ public void doHandleRequestTagging(
+ @NotNull Span span, @NotNull RequestBuilderWrapper request
+ ) {
+ tagAndNamingStrategy.handleRequestTagging(span, request, tagAndNamingAdapter);
+ }
+
+ @Override
+ public void doHandleResponseTaggingAndFinalSpanName(
+ @NotNull Span span,
+ @Nullable RequestBuilderWrapper request,
+ @Nullable Response response,
+ @Nullable Throwable error
+ ) {
+ tagAndNamingStrategy.handleResponseTaggingAndFinalSpanName(
+ span, request, response, error, tagAndNamingAdapter
+ );
+ }
+}
diff --git a/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/RequestBuilderWrapper.java b/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/RequestBuilderWrapper.java
new file mode 100644
index 00000000..1e6c7fe4
--- /dev/null
+++ b/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/RequestBuilderWrapper.java
@@ -0,0 +1,118 @@
+package com.nike.riposte.client.asynchttp;
+
+import com.nike.fastbreak.CircuitBreaker;
+
+import org.asynchttpclient.BoundRequestBuilder;
+import org.asynchttpclient.Response;
+
+import java.util.Optional;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.HttpMethod;
+
+/**
+ * Wrapper around the actual {@link #requestBuilder} that also keeps track of the URL and HTTP method used to create the
+ * builder (since there are no getters to inspect the builder). Generate one of these by calling one of the
+ * request-starter methods in {@link AsyncHttpClientHelper}, e.g. {@link
+ * AsyncHttpClientHelper#getRequestBuilder(String, HttpMethod)}. From there you'll want to set any headers, add request
+ * body, or adjust anything else about the request by interacting with {@link #requestBuilder} before passing it into
+ * one of the execute methods (e.g. {@link
+ * AsyncHttpClientHelper#executeAsyncHttpRequest(RequestBuilderWrapper, AsyncResponseHandler, ChannelHandlerContext)}).
+ *
+ * @author Nic Munroe
+ */
+@SuppressWarnings({"WeakerAccess", "OptionalUsedAsFieldOrParameterType"})
+public class RequestBuilderWrapper {
+
+ String url;
+ String httpMethod;
+ public final BoundRequestBuilder requestBuilder;
+ /**
+ * An Optional containing a custom circuit breaker if a custom one should be used, or empty if the request sender
+ * should use a default circuit breaker. If you don't want *any* circuit breaker to be used, set {@link
+ * #disableCircuitBreaker} to true. The default circuit breaker will be based on the host value of the {@link #url}
+ * (i.e. all calls to the same host will use the same circuit breaker). If you need something more (or less) fine
+ * grained than that then you'll need to provide a custom circuit breaker.
+ */
+ Optional> customCircuitBreaker;
+ /**
+ * Set this to true if you don't want *any* circuit breaker to be used - if this is false then {@link
+ * #customCircuitBreaker} will be used to determine which circuit breaker to use (custom vs. default).
+ */
+ boolean disableCircuitBreaker;
+
+ private ChannelHandlerContext ctx;
+
+ /**
+ * Intentionally package-scoped. Instances of this class are generated and returned by calling one of the
+ * request-starter methods in {@link AsyncHttpClientHelper}, e.g. {@link
+ * AsyncHttpClientHelper#getRequestBuilder(String, HttpMethod)}.
+ */
+ RequestBuilderWrapper(
+ String url, String httpMethod, BoundRequestBuilder requestBuilder,
+ Optional> customCircuitBreaker, boolean disableCircuitBreaker
+ ) {
+ this.url = url;
+ this.httpMethod = httpMethod;
+ this.requestBuilder = requestBuilder;
+ this.customCircuitBreaker = customCircuitBreaker;
+ this.disableCircuitBreaker = disableCircuitBreaker;
+ }
+
+ ChannelHandlerContext getCtx() {
+ return ctx;
+ }
+
+ void setCtx(ChannelHandlerContext ctx) {
+ this.ctx = ctx;
+ }
+
+ public void setCustomCircuitBreaker(Optional> customCircuitBreaker) {
+ this.customCircuitBreaker = customCircuitBreaker;
+ }
+
+ public Optional> getCustomCircuitBreaker() {
+ return customCircuitBreaker;
+ }
+
+ public void setDisableCircuitBreaker(boolean disableCircuitBreaker) {
+ this.disableCircuitBreaker = disableCircuitBreaker;
+ }
+
+ public boolean isDisableCircuitBreaker() {
+ return disableCircuitBreaker;
+ }
+
+ /**
+ * Use this method to update the url stored inside this {@link RequestBuilderWrapper}
+ * and the wrapped {@link BoundRequestBuilder}
+ *
+ *
Setting the url only on the wrapped {@link BoundRequestBuilder} will impact logging
+ * and circuit breakers potentially. Use this method to keep the two in sync.
+ */
+ public void setUrl(String url) {
+ this.url = url;
+ requestBuilder.setUrl(url);
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ /**
+ *
Use this method to update the httpMethod stored inside this {@link RequestBuilderWrapper}
+ * and the wrapped {@link BoundRequestBuilder}
+ *
+ *
Setting the httpMethod only on the wrapped {@link BoundRequestBuilder} will impact logging
+ * and circuit breakers potentially. Use this method to keep the two in sync.
+ */
+ public void setHttpMethod(String httpMethod) {
+ this.httpMethod = httpMethod;
+ requestBuilder.setMethod(httpMethod);
+ }
+
+ public String getHttpMethod() {
+ return httpMethod;
+ }
+
+}
diff --git a/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/util/AwsUtil.java b/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/util/AwsUtil.java
new file mode 100644
index 00000000..36605066
--- /dev/null
+++ b/riposte-async-http-client2/src/main/java/com/nike/riposte/client/asynchttp/util/AwsUtil.java
@@ -0,0 +1,244 @@
+package com.nike.riposte.client.asynchttp.util;
+
+import com.nike.riposte.client.asynchttp.AsyncHttpClientHelper;
+import com.nike.riposte.server.config.AppInfo;
+import com.nike.riposte.server.config.impl.AppInfoImpl;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.asynchttpclient.Response;
+
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import io.netty.handler.codec.http.HttpMethod;
+
+/**
+ * Helper class for dealing with AWS related stuff. In particular {@link #getAppInfoFutureWithAwsInfo(String, String,
+ * AsyncHttpClientHelper)} or {@link #getAppInfoFutureWithAwsInfo(AsyncHttpClientHelper)} will give you a {@link
+ * CompletableFuture} that will return an {@link AppInfo} with the {@link AppInfo#dataCenter()} and {@link
+ * AppInfo#instanceId()} pulled from the AWS metadata services.
+ *
+ * @author Nic Munroe
+ */
+@SuppressWarnings("WeakerAccess")
+public class AwsUtil {
+
+ private static final Logger logger = LoggerFactory.getLogger(AwsUtil.class);
+
+ /**
+ * The base IP of the magic URLs that you can call from an AWS instance to get information about that instance.
+ */
+ public static final String AMAZON_METADATA_URL_BASE = "http://169.254.169.254";
+ /**
+ * If you call this URL from an AWS instance you'll get back a simple string response that looks like: i-abc12d3e
+ */
+ public static final String AMAZON_METADATA_INSTANCE_ID_URL =
+ AMAZON_METADATA_URL_BASE + "/latest/meta-data/instance-id";
+ /**
+ * If you call this URL from an AWS instance you'll get back a simple string response that looks like: us-west-2b
+ * NOTE: Don't confuse this with region - they are slightly different (the region for this us-west-2b availability
+ * zone would be us-west-2).
+ */
+ @SuppressWarnings("unused")
+ public static final String AMAZON_METADATA_AVAILABILITY_ZONE_URL =
+ AMAZON_METADATA_URL_BASE + "/latest/meta-data/placement/availability-zone";
+ /**
+ * If you call this URL from an AWS instance you'll get back a JSON string that looks like:
+ *
+ * {
+ * "devpayProductCodes" : null,
+ * "privateIp" : "123.45.67.89",
+ * "availabilityZone" : "us-west-2b",
+ * "version" : "2010-08-31",
+ * "accountId" : "111222333444",
+ * "instanceId" : "i-aaa11b2c",
+ * "billingProducts" : null,
+ * "imageId" : "ami-1111a222",
+ * "instanceType" : "m3.medium",
+ * "kernelId" : null,
+ * "ramdiskId" : null,
+ * "architecture" : "x86_64",
+ * "pendingTime" : "2015-04-06T19:49:49Z",
+ * "region" : "us-west-2"
+ * }
+ *
+ */
+ public static final String AMAZON_METADATA_DOCUMENT_URL =
+ AMAZON_METADATA_URL_BASE + "/latest/dynamic/instance-identity/document";
+
+ // Intentionally protected - use the static methods
+ protected AwsUtil() { /* do nothing */ }
+
+ /**
+ * @param asyncHttpClientHelper The async HTTP client you want this method to use to make the AWS metadata call.
+ *
+ * @return A {@link CompletableFuture} that will contain the AWS region this app is running in (assuming it
+ * completes successfully). If an error occurs retrieving the region from AWS then the error will be logged and
+ * {@link AppInfo#UNKNOWN_VALUE} returned as the value.
+ */
+ public static @NotNull CompletableFuture<@NotNull String> getAwsRegion(
+ @NotNull AsyncHttpClientHelper asyncHttpClientHelper
+ ) {
+ return asyncHttpClientHelper.executeAsyncHttpRequest(
+ asyncHttpClientHelper.getRequestBuilder(AMAZON_METADATA_DOCUMENT_URL, HttpMethod.GET),
+ response -> {
+ String region = null;
+ try {
+ ObjectMapper objectMapper = new ObjectMapper();
+ Map resultMap =
+ objectMapper.readValue(response.getResponseBody(), new TypeReference