Skip to content

Commit

Permalink
Apache httpasyncclient 5.x (#5697)
Browse files Browse the repository at this point in the history
* Copies code for httpasyncclient-4.1 and creates instrumentation for 5.0

* Makes import changes for http client 5

* Decorate request channel and changes type in tests

* Corrects test cases

* Corrects most of the test cases

* Forces http1 protocol to pass the test cases

* Merge supported libraries for async client

Co-authored-by: Mateusz Rzeszutek <[email protected]>

* Remove http.sceme and http.target attr from test

Co-authored-by: Mateusz Rzeszutek <[email protected]>

* Removes not needed null check for status code

* Replaces slf4j loggers with JUL

* Inlined flavor extraction in attributes getter

* Uses parameter placeholders for logging

* Uses success endpoint to test flavor

* Merges httpasyncclient and httpclient modules

* Merges http client 5 modules

* Update java-8 compatible changes

Co-authored-by: Mateusz Rzeszutek <[email protected]>

* Change instrumentation name

Co-authored-by: Mateusz Rzeszutek <[email protected]>

* Adds missing import statement

* Rename packages

* Java 8

* Reverts adding 5.0+ support from supporting libraries

* Deleted hanging module

* Uses seconds instead of ms in http test

* Merges both classic and async client implementations

* Moves http client all test cases to java tests

* Uses abstract apache test class and moves boilerplate

* Uses connection and read timeouts from ApacheHttpClientTest

* Refactors remaining classes, shifts logic to HttpUtils

* Renames HttpUtils to ApacheHttpClientUtils

* Corrects failing code style error

* Corrects build errors

* Renames package to have http client version

* Corrects package name

* Uses instrumenter as static import

* Inline utility methods

Co-authored-by: Mateusz Rzeszutek <[email protected]>
Co-authored-by: Trask Stalnaker <[email protected]>
  • Loading branch information
3 people authored Apr 7, 2022
1 parent a150111 commit 13a851b
Show file tree
Hide file tree
Showing 12 changed files with 780 additions and 265 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0;

import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface;
import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientSingletons.instrumenter;
import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext;
import static java.util.logging.Level.FINE;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import java.io.IOException;
import java.util.logging.Logger;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.EntityDetails;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.nio.AsyncRequestProducer;
import org.apache.hc.core5.http.nio.DataStreamChannel;
import org.apache.hc.core5.http.nio.RequestChannel;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.protocol.HttpCoreContext;

class ApacheHttpAsyncClientInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("org.apache.hc.client5.http.async.HttpAsyncClient");
}

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return implementsInterface(named("org.apache.hc.client5.http.async.HttpAsyncClient"));
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
isMethod()
.and(named("execute"))
.and(takesArguments(5))
.and(takesArgument(0, named("org.apache.hc.core5.http.nio.AsyncRequestProducer")))
.and(takesArgument(1, named("org.apache.hc.core5.http.nio.AsyncResponseConsumer")))
.and(takesArgument(2, named("org.apache.hc.core5.http.nio.HandlerFactory")))
.and(takesArgument(3, named("org.apache.hc.core5.http.protocol.HttpContext")))
.and(takesArgument(4, named("org.apache.hc.core5.concurrent.FutureCallback"))),
this.getClass().getName() + "$ClientAdvice");
}

@SuppressWarnings("unused")
public static class ClientAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void methodEnter(
@Advice.Argument(value = 0, readOnly = false) AsyncRequestProducer requestProducer,
@Advice.Argument(3) HttpContext httpContext,
@Advice.Argument(value = 4, readOnly = false) FutureCallback<?> futureCallback) {

Context parentContext = currentContext();

WrappedFutureCallback<?> wrappedFutureCallback =
new WrappedFutureCallback<>(parentContext, httpContext, futureCallback);
requestProducer =
new DelegatingRequestProducer(parentContext, requestProducer, wrappedFutureCallback);
futureCallback = wrappedFutureCallback;
}
}

public static class DelegatingRequestProducer implements AsyncRequestProducer {
private final Context parentContext;
private final AsyncRequestProducer delegate;
private final WrappedFutureCallback<?> wrappedFutureCallback;

public DelegatingRequestProducer(
Context parentContext,
AsyncRequestProducer delegate,
WrappedFutureCallback<?> wrappedFutureCallback) {
this.parentContext = parentContext;
this.delegate = delegate;
this.wrappedFutureCallback = wrappedFutureCallback;
}

@Override
public void failed(Exception ex) {
delegate.failed(ex);
}

@Override
public void sendRequest(RequestChannel channel, HttpContext context)
throws HttpException, IOException {
DelegatingRequestChannel requestChannel =
new DelegatingRequestChannel(channel, parentContext, wrappedFutureCallback);
delegate.sendRequest(requestChannel, context);
}

@Override
public boolean isRepeatable() {
return delegate.isRepeatable();
}

@Override
public int available() {
return delegate.available();
}

@Override
public void produce(DataStreamChannel channel) throws IOException {
delegate.produce(channel);
}

@Override
public void releaseResources() {
delegate.releaseResources();
}
}

public static class DelegatingRequestChannel implements RequestChannel {
private final RequestChannel delegate;
private final Context parentContext;
private final WrappedFutureCallback<?> wrappedFutureCallback;

public DelegatingRequestChannel(
RequestChannel requestChannel,
Context parentContext,
WrappedFutureCallback<?> wrappedFutureCallback) {
this.delegate = requestChannel;
this.parentContext = parentContext;
this.wrappedFutureCallback = wrappedFutureCallback;
}

@Override
public void sendRequest(HttpRequest request, EntityDetails entityDetails, HttpContext context)
throws HttpException, IOException {
if (instrumenter().shouldStart(parentContext, request)) {
wrappedFutureCallback.context = instrumenter().start(parentContext, request);
wrappedFutureCallback.httpRequest = request;
}

delegate.sendRequest(request, entityDetails, context);
}
}

public static class WrappedFutureCallback<T> implements FutureCallback<T> {

private static final Logger logger = Logger.getLogger(WrappedFutureCallback.class.getName());

private final Context parentContext;
private final HttpContext httpContext;
private final FutureCallback<T> delegate;

private volatile Context context;
private volatile HttpRequest httpRequest;

public WrappedFutureCallback(
Context parentContext, HttpContext httpContext, FutureCallback<T> delegate) {
this.parentContext = parentContext;
this.httpContext = httpContext;
// Note: this can be null in real life, so we have to handle this carefully
this.delegate = delegate;
}

@Override
public void completed(T result) {
if (context == null) {
// this is unexpected
logger.log(FINE, "context was never set");
completeDelegate(result);
return;
}

instrumenter().end(context, httpRequest, getResponse(httpContext), null);

if (parentContext == null) {
completeDelegate(result);
return;
}

try (Scope ignored = parentContext.makeCurrent()) {
completeDelegate(result);
}
}

@Override
public void failed(Exception ex) {
if (context == null) {
// this is unexpected
logger.log(FINE, "context was never set");
failDelegate(ex);
return;
}

// end span before calling delegate
instrumenter().end(context, httpRequest, getResponse(httpContext), ex);

if (parentContext == null) {
failDelegate(ex);
return;
}

try (Scope ignored = parentContext.makeCurrent()) {
failDelegate(ex);
}
}

@Override
public void cancelled() {
if (context == null) {
// this is unexpected
logger.log(FINE, "context was never set");
cancelDelegate();
return;
}

// TODO (trask) add "canceled" span attribute
// end span before calling delegate
instrumenter().end(context, httpRequest, getResponse(httpContext), null);

if (parentContext == null) {
cancelDelegate();
return;
}

try (Scope ignored = parentContext.makeCurrent()) {
cancelDelegate();
}
}

private void completeDelegate(T result) {
if (delegate != null) {
delegate.completed(result);
}
}

private void failDelegate(Exception ex) {
if (delegate != null) {
delegate.failed(ex);
}
}

private void cancelDelegate() {
if (delegate != null) {
delegate.cancelled();
}
}

private static HttpResponse getResponse(HttpContext context) {
return (HttpResponse) context.getAttribute(HttpCoreContext.HTTP_RESPONSE);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,25 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.MessageHeaders;
import org.apache.hc.core5.http.ProtocolVersion;
import org.apache.hc.core5.net.URIAuthority;

final class ApacheHttpClientHttpAttributesGetter
implements HttpClientAttributesGetter<ClassicHttpRequest, HttpResponse> {

implements HttpClientAttributesGetter<HttpRequest, HttpResponse> {
private static final Logger logger =
Logger.getLogger(ApacheHttpClientHttpAttributesGetter.class.getName());

@Override
public String method(ClassicHttpRequest request) {
public String method(HttpRequest request) {
return request.getMethod();
}

@Override
public String url(ClassicHttpRequest request) {
public String url(HttpRequest request) {
// similar to org.apache.hc.core5.http.message.BasicHttpRequest.getUri()
// not calling getUri() to avoid unnecessary conversion
StringBuilder url = new StringBuilder();
Expand Down Expand Up @@ -64,34 +64,34 @@ public String url(ClassicHttpRequest request) {
}

@Override
public List<String> requestHeader(ClassicHttpRequest request, String name) {
return headersToList(request.getHeaders(name));
public List<String> requestHeader(HttpRequest request, String name) {
return getHeader(request, name);
}

@Override
@Nullable
public Long requestContentLength(ClassicHttpRequest request, @Nullable HttpResponse response) {
public Long requestContentLength(HttpRequest request, @Nullable HttpResponse response) {
return null;
}

@Override
@Nullable
public Long requestContentLengthUncompressed(
ClassicHttpRequest request, @Nullable HttpResponse response) {
HttpRequest request, @Nullable HttpResponse response) {
return null;
}

@Override
public Integer statusCode(ClassicHttpRequest request, HttpResponse response) {
public Integer statusCode(HttpRequest request, HttpResponse response) {
return response.getCode();
}

@Override
@Nullable
public String flavor(ClassicHttpRequest request, @Nullable HttpResponse response) {
ProtocolVersion protocolVersion = request.getVersion();
public String flavor(HttpRequest request, @Nullable HttpResponse response) {
ProtocolVersion protocolVersion = getVersion(request, response);
if (protocolVersion == null) {
return SemanticAttributes.HttpFlavorValues.HTTP_1_1;
return null;
}
String protocol = protocolVersion.getProtocol();
if (!protocol.equals("HTTP")) {
Expand All @@ -114,20 +114,31 @@ public String flavor(ClassicHttpRequest request, @Nullable HttpResponse response

@Override
@Nullable
public Long responseContentLength(ClassicHttpRequest request, HttpResponse response) {
public Long responseContentLength(HttpRequest request, HttpResponse response) {
return null;
}

@Override
@Nullable
public Long responseContentLengthUncompressed(ClassicHttpRequest request, HttpResponse response) {
public Long responseContentLengthUncompressed(HttpRequest request, HttpResponse response) {
return null;
}

@Override
public List<String> responseHeader(
ClassicHttpRequest request, HttpResponse response, String name) {
return headersToList(response.getHeaders(name));
public List<String> responseHeader(HttpRequest request, HttpResponse response, String name) {
return getHeader(response, name);
}

private static ProtocolVersion getVersion(HttpRequest request, @Nullable HttpResponse response) {
ProtocolVersion protocolVersion = request.getVersion();
if (protocolVersion == null && response != null) {
protocolVersion = response.getVersion();
}
return protocolVersion;
}

private static List<String> getHeader(MessageHeaders messageHeaders, String name) {
return headersToList(messageHeaders.getHeaders(name));
}

// minimize memory overhead by not using streams
Expand All @@ -136,8 +147,8 @@ private static List<String> headersToList(Header[] headers) {
return Collections.emptyList();
}
List<String> headersList = new ArrayList<>(headers.length);
for (int i = 0; i < headers.length; ++i) {
headersList.add(headers[i].getValue());
for (Header header : headers) {
headersList.add(header.getValue());
}
return headersList;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;

public class ApacheHttpClientInstrumentation implements TypeInstrumentation {
class ApacheHttpClientInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("org.apache.hc.client5.http.classic.HttpClient");
Expand Down
Loading

0 comments on commit 13a851b

Please sign in to comment.