-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Apache httpasyncclient 5.x (open-telemetry#5697)
* 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
Showing
12 changed files
with
780 additions
and
265 deletions.
There are no files selected for viewing
264 changes: 264 additions & 0 deletions
264
...javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpAsyncClientInstrumentation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.